rpcserver: implement SendPaymentSync and OpenChannelSync

This RPC implements the two new RPC methods added in a prior commit.
This involved a slight refactoring to make use of duplicated code
amongst the sync and async variants of the methods.
This commit is contained in:
Olaoluwa Osuntokun 2016-11-10 17:37:21 -08:00
parent 64396a69c3
commit ccd0f57cdf
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
@ -236,11 +237,16 @@ func (r *rpcServer) OpenChannel(in *lnrpc.OpenChannelRequest,
localFundingAmt := btcutil.Amount(in.LocalFundingAmount) localFundingAmt := btcutil.Amount(in.LocalFundingAmount)
remoteFundingAmt := btcutil.Amount(in.RemoteFundingAmount) remoteFundingAmt := btcutil.Amount(in.RemoteFundingAmount)
// TODO(roasbeef): make it optional
nodepubKey, err := btcec.ParsePubKey(in.NodePubkey, btcec.S256()) nodepubKey, err := btcec.ParsePubKey(in.NodePubkey, btcec.S256())
if err != nil { if err != nil {
return err return err
} }
// Instruct the server to trigger the necessary events to attempt to
// open a new channel. A stream is returned in place, this stream will
// be used to consume updates of the state of the pending channel.
updateChan, errChan := r.server.OpenChannel(in.TargetPeerId, updateChan, errChan := r.server.OpenChannel(in.TargetPeerId,
nodepubKey, localFundingAmt, remoteFundingAmt, in.NumConfs) nodepubKey, localFundingAmt, remoteFundingAmt, in.NumConfs)
@ -284,6 +290,63 @@ out:
return nil return nil
} }
// OpenChannelSync is a synchronous version of the OpenChannel RPC call. This
// call is meant to be consumed by clients to the REST proxy. As with all other
// sync calls, all byte slices are instead to be populated as hex encoded
// strings.
func (r *rpcServer) OpenChannelSync(ctx context.Context,
in *lnrpc.OpenChannelRequest) (*lnrpc.ChannelPoint, error) {
rpcsLog.Tracef("[openchannel] request to peerid(%v) "+
"allocation(us=%v, them=%v) numconfs=%v", in.TargetPeerId,
in.LocalFundingAmount, in.RemoteFundingAmount, in.NumConfs)
// Decode the provided target node's public key, parsing it into a pub
// key object. For all sync call, byte slices are expected to be
// encoded as hex strings.
keyBytes, err := hex.DecodeString(in.NodePubkeyString)
if err != nil {
return nil, err
}
nodepubKey, err := btcec.ParsePubKey(keyBytes, btcec.S256())
if err != nil {
return nil, err
}
localFundingAmt := btcutil.Amount(in.LocalFundingAmount)
remoteFundingAmt := btcutil.Amount(in.RemoteFundingAmount)
updateChan, errChan := r.server.OpenChannel(in.TargetPeerId,
nodepubKey, localFundingAmt, remoteFundingAmt, in.NumConfs)
select {
// If an error occurs them immediately return the error to the client.
case err := <-errChan:
rpcsLog.Errorf("unable to open channel to "+
"identityPub(%x) nor peerID(%v): %v",
nodepubKey, in.TargetPeerId, err)
return nil, err
// Otherwise, wait for the first channel update. The first update sent
// is when the funding transaction is broadcast to the network.
case fundingUpdate := <-updateChan:
rpcsLog.Tracef("[openchannel] sending update: %v",
fundingUpdate)
// Parse out the txid of the pending funding transaction. The
// sync client can use this to poll against the list of
// PendingChannels.
openUpdate := fundingUpdate.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
chanUpdate := openUpdate.ChanPending
return &lnrpc.ChannelPoint{
FundingTxid: chanUpdate.Txid,
}, nil
case <-r.quit:
return nil, nil
}
}
// CloseChannel attempts to close an active channel identified by its channel // CloseChannel attempts to close an active channel identified by its channel
// point. The actions of this method can additionally be augmented to attempt // point. The actions of this method can additionally be augmented to attempt
// a force close after a timeout period in the case of an inactive peer. // a force close after a timeout period in the case of an inactive peer.
@ -515,7 +578,6 @@ func (r *rpcServer) ListChannels(ctx context.Context,
// bi-directional stream allowing clients to rapidly send payments through the // bi-directional stream allowing clients to rapidly send payments through the
// Lightning Network with a single persistent connection. // Lightning Network with a single persistent connection.
func (r *rpcServer) SendPayment(paymentStream lnrpc.Lightning_SendPaymentServer) error { func (r *rpcServer) SendPayment(paymentStream lnrpc.Lightning_SendPaymentServer) error {
const queryTimeout = time.Duration(time.Second * 10)
errChan := make(chan error, 1) errChan := make(chan error, 1)
payChan := make(chan *lnrpc.SendRequest) payChan := make(chan *lnrpc.SendRequest)
@ -552,65 +614,37 @@ func (r *rpcServer) SendPayment(paymentStream lnrpc.Lightning_SendPaymentServer)
case err := <-errChan: case err := <-errChan:
return err return err
case nextPayment := <-payChan: case nextPayment := <-payChan:
// Query the routing table for a potential path to the
// destination node. If a path is ultimately
// unavailable, then an error will be returned.
destNode := hex.EncodeToString(nextPayment.Dest)
targetVertex := graph.NewID(destNode)
path, err := r.server.routingMgr.FindPath(targetVertex,
queryTimeout)
if err != nil {
return err
}
rpcsLog.Tracef("[sendpayment] selected route: %v", path)
// If we're in debug HTLC mode, then all outgoing // If we're in debug HTLC mode, then all outgoing
// HTLC's will pay to the same debug rHash. Otherwise, // HTLC's will pay to the same debug rHash. Otherwise,
// we pay to the rHash specified within the RPC // we pay to the rHash specified within the RPC
// request. // request.
var rHash [32]byte var rHash [32]byte
if cfg.DebugHTLC { if cfg.DebugHTLC && len(nextPayment.PaymentHash) == 0 {
rHash = debugHash rHash = debugHash
} else { } else {
copy(rHash[:], nextPayment.PaymentHash) copy(rHash[:], nextPayment.PaymentHash)
} }
// Generate the raw encoded sphinx packet to be // Construct and HTLC packet which a payment route (if
// included along with the HTLC add message. We snip // one is found) to the destination using a Sphinx
// off the first hop from the path as within the // onoin packet to encode the route.
// routing table's star graph, we're always the first dest := hex.EncodeToString(nextPayment.Dest)
// hop. htlcPkt, err := r.constructPaymentRoute(dest,
sphinxPacket, err := generateSphinxPacket(path[1:], rHash[:]) nextPayment.Amt, rHash)
if err != nil { if err != nil {
return err return err
} }
// Craft an HTLC packet to send to the routing // We launch a new goroutine to execute the current
// sub-system. The meta-data within this packet will be // payment so we can continue to serve requests while
// used to route the payment through the network. // this payment is being dispatiched.
htlcAdd := &lnwire.HTLCAddRequest{ //
Amount: lnwire.CreditsAmount(nextPayment.Amt),
RedemptionHashes: [][32]byte{rHash},
OnionBlob: sphinxPacket,
}
firstHopPub, err := hex.DecodeString(path[1].String())
if err != nil {
return err
}
destAddr := wire.ShaHash(fastsha256.Sum256(firstHopPub))
htlcPkt := &htlcPacket{
dest: destAddr,
msg: htlcAdd,
}
// TODO(roasbeef): semaphore to limit num outstanding // TODO(roasbeef): semaphore to limit num outstanding
// goroutines. // goroutines.
go func() { go func() {
// Finally, send this next packet to the // Finally, send this next packet to the
// routing layer in order to complete the next // routing layer in order to complete the next
// payment. // payment.
// TODO(roasbeef): this should go through the
// L3 router once multi-hop is in place.
if err := r.server.htlcSwitch.SendHTLC(htlcPkt); err != nil { if err := r.server.htlcSwitch.SendHTLC(htlcPkt); err != nil {
errChan <- err errChan <- err
return return
@ -629,6 +663,95 @@ func (r *rpcServer) SendPayment(paymentStream lnrpc.Lightning_SendPaymentServer)
return nil return nil
} }
// SendPaymentSync is the synchronous non-streaming version of SendPayment.
// This RPC is intended to be consumed by clients of the REST proxy.
// Additionally, this RPC expects the destination's public key and the payment
// hash (if any) to be encoded as hex strings.
func (r *rpcServer) SendPaymentSync(ctx context.Context,
nextPayment *lnrpc.SendRequest) (*lnrpc.SendResponse, error) {
// If we're in debug HTLC mode, then all outgoing HTLC's will pay to
// the same debug rHash. Otherwise, we pay to the rHash specified
// within the RPC request.
var rHash [32]byte
if cfg.DebugHTLC && nextPayment.PaymentHashString == "" {
rHash = debugHash
} else {
paymentHash, err := hex.DecodeString(nextPayment.PaymentHashString)
if err != nil {
return nil, err
}
copy(rHash[:], paymentHash)
}
// Construct and HTLC packet which a payment route (if
// one is found) to the destination using a Sphinx
// onoin packet to encode the route.
htlcPkt, err := r.constructPaymentRoute(nextPayment.DestString,
nextPayment.Amt, rHash)
if err != nil {
return nil, err
}
// Finally, send this next packet to the routing layer in order to
// complete the next payment.
if err := r.server.htlcSwitch.SendHTLC(htlcPkt); err != nil {
return nil, err
}
return &lnrpc.SendResponse{}, nil
}
// constructPaymentRoute attempts to construct a complete HTLC packet which
// encapsulates a Sphinx onion packet that encodes the end-to-end route any
// payment instructions necessary to complete an HTLC. If a route is unable to
// be located, then an error is returned indicating as much.
func (r *rpcServer) constructPaymentRoute(destPubkey string, amt int64,
rHash [32]byte) (*htlcPacket, error) {
const queryTimeout = time.Duration(time.Second * 10)
// Query the routing table for a potential path to the destination
// node. If a path is ultimately unavailable, then an error will be
// returned.
targetVertex := graph.NewID(destPubkey)
path, err := r.server.routingMgr.FindPath(targetVertex,
queryTimeout)
if err != nil {
return nil, err
}
rpcsLog.Tracef("[sendpayment] selected route: %v", path)
// Generate the raw encoded sphinx packet to be included along with the
// HTLC add message. We snip off the first hop from the path as within
// the routing table's star graph, we're always the first hop.
sphinxPacket, err := generateSphinxPacket(path[1:], rHash[:])
if err != nil {
return nil, err
}
// Craft an HTLC packet to send to the routing sub-system. The
// meta-data within this packet will be used to route the payment
// through the network.
htlcAdd := &lnwire.HTLCAddRequest{
Amount: lnwire.CreditsAmount(amt),
RedemptionHashes: [][32]byte{rHash},
OnionBlob: sphinxPacket,
}
firstHopPub, err := hex.DecodeString(path[1].String())
if err != nil {
return nil, err
}
destInterface := wire.ShaHash(fastsha256.Sum256(firstHopPub))
return &htlcPacket{
dest: destInterface,
msg: htlcAdd,
}, nil
}
// generateSphinxPacket generates then encodes a sphinx packet which encodes // generateSphinxPacket generates then encodes a sphinx packet which encodes
// the onion route specified by the passed list of graph vertexes. The blob // the onion route specified by the passed list of graph vertexes. The blob
// returned from this function can immediately be included within an HTLC add // returned from this function can immediately be included within an HTLC add
@ -703,13 +826,30 @@ func generateSphinxPacket(vertexes []graph.ID, paymentHash []byte) ([]byte, erro
func (r *rpcServer) AddInvoice(ctx context.Context, func (r *rpcServer) AddInvoice(ctx context.Context,
invoice *lnrpc.Invoice) (*lnrpc.AddInvoiceResponse, error) { invoice *lnrpc.Invoice) (*lnrpc.AddInvoiceResponse, error) {
preImage := invoice.RPreimage var paymentPreimage [32]byte
preimageLength := len(preImage)
if preimageLength != 32 { switch {
// If a preimage wasn't specified, then we'll generate a new preimage
// from fresh cryptographic randomness.
case len(invoice.RPreimage) == 0:
if _, err := rand.Read(paymentPreimage[:]); err != nil {
return nil, err
}
// Otherwise, if a preimage was specified, then it MUST be exactly
// 32-bytes.
case len(invoice.RPreimage) > 0 && len(invoice.RPreimage) != 32:
return nil, fmt.Errorf("payment preimage must be exactly "+ return nil, fmt.Errorf("payment preimage must be exactly "+
"32 bytes, is instead %v", preimageLength) "32 bytes, is instead %v", len(invoice.RPreimage))
// If the preimage meets the size specifications, then it can be used
// as is.
default:
copy(paymentPreimage[:], invoice.RPreimage[:])
} }
// The size of the memo and receipt attached must not exceed the
// maximum values for either of the fields.
if len(invoice.Memo) > channeldb.MaxMemoSize { if len(invoice.Memo) > channeldb.MaxMemoSize {
return nil, fmt.Errorf("memo too large: %v bytes "+ return nil, fmt.Errorf("memo too large: %v bytes "+
"(maxsize=%v)", len(invoice.Memo), channeldb.MaxMemoSize) "(maxsize=%v)", len(invoice.Memo), channeldb.MaxMemoSize)
@ -727,18 +867,23 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
Value: btcutil.Amount(invoice.Value), Value: btcutil.Amount(invoice.Value),
}, },
} }
copy(i.Terms.PaymentPreimage[:], preImage) copy(i.Terms.PaymentPreimage[:], paymentPreimage[:])
rpcsLog.Tracef("[addinvoice] adding new invoice %v", rpcsLog.Tracef("[addinvoice] adding new invoice %v",
newLogClosure(func() string { newLogClosure(func() string {
return spew.Sdump(i) return spew.Sdump(i)
})) }))
// With all sanity checks passed, write the invoice to the database.
if err := r.server.invoices.AddInvoice(i); err != nil { if err := r.server.invoices.AddInvoice(i); err != nil {
return nil, err return nil, err
} }
rHash := fastsha256.Sum256(preImage) // Finally generate the payment hash itself from the pre-image. This
// will be used by clients to query for the state of a particular
// invoice.
rHash := fastsha256.Sum256(paymentPreimage[:])
return &lnrpc.AddInvoiceResponse{ return &lnrpc.AddInvoiceResponse{
RHash: rHash[:], RHash: rHash[:],
}, nil }, nil
@ -902,6 +1047,7 @@ func (r *rpcServer) SubscribeTransactions(req *lnrpc.GetTransactionsRequest,
func (r *rpcServer) GetTransactions(context.Context, func (r *rpcServer) GetTransactions(context.Context,
*lnrpc.GetTransactionsRequest) (*lnrpc.TransactionDetails, error) { *lnrpc.GetTransactionsRequest) (*lnrpc.TransactionDetails, error) {
// TODO(roasbeef): add pagination support
transactions, err := r.server.lnwallet.ListTransactionDetails() transactions, err := r.server.lnwallet.ListTransactionDetails()
if err != nil { if err != nil {
return nil, err return nil, err