diff --git a/.gitignore b/.gitignore index f1c181e..1628276 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,59 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll +# ---> Go +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a *.so -*.dylib -# Test binary, build with `go test -c` +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe *.test +*.prof + +output*.log + +swapcli +!swapcli/ + +*.key +*.hex + +# vim +*.swp + +*.hex +*.db +*.bin + +vendor +*.idea +*.iml +profile.cov +profile.tmp + +.DS_Store + +.vscode + +nautserver +!nautserver/ + +nautview +!nautview/ -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +swapd +!swapd/ \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..caa7c12 --- /dev/null +++ b/client/README.md @@ -0,0 +1,96 @@ +# Swaplet + +## Uncharge swap (off -> on-chain) + +``` + swapcli uncharge 500 + | + | + v + .-----------------------------. + | Swap CLI | + | ./cmd/swapcli | + | | + | | + | .-------------------. | .--------------. .---------------. + | | Swap Client (lib) | | | LND node | | Bitcoin node | + | | ./ |<-------------| |-------------------| | + | | | | | | on-chain | | + | | |------------->| | htlc | | + | | | | off-chain | | | | + | '-------------------' | htlc '--------------' '---------------' + '-----------------|-----------' | ^ + | | | + | v | + | .--. .--. + | _ -( )- _ _ -( )- _ + | .--,( ),--. .--,( ),--. + initiate| _.-( )-._ _.-( )-._ + swap | ( LIGHTNING NETWORK ) ( BITCOIN NETWORK ) + | '-._( )_.-' '-._( )_.-' + | '__,( ),__' '__,( ),__' + | - ._(__)_. - - ._(__)_. - + | | ^ + | | | + v v | + .--------------------. off-chain .--------------. .---------------. + | Swap Server | htlc | LND node | | Bitcoin node | + | |<-------------| | | | + | | | | on-chain | | + | | | | htlc | | + | |--------------| |----------------->| | + | | | | | | + '--------------------' '--------------' '---------------' + +``` + +## Setup + +LND and the swaplet are using go modules. Make sure that the `GO111MODULE` env variable is set to `on`. + +In order to execute a swap, LND needs to be rebuilt with sub servers enabled. + +### LND + +* Checkout branch `master` + +- `make install tags="signrpc walletrpc chainrpc"` to build and install lnd with required sub-servers enabled. + +- Make sure there are no macaroons in the lnd dir `~/.lnd/data/chain/bitcoin/mainnet`. If there are, lnd has been started before and in that case, it could be that `admin.macaroon` doesn't contain signer permission. Delete `macaroons.db` and `*.macaroon`. + + DO NOT DELETE `wallet.db` ! + +- Start lnd + +### Swaplet +- `git clone git@gitlab.com:lightning-labs/swaplet.git` +- `cd swaplet/cmd` +- `go install ./...` + +## Execute a swap + +* Swaps are executed by a client daemon process. Run: + + `swapd` + + By default `swapd` attempts to connect to an lnd instance running on `localhost:10009` and reads the macaroon and tls certificate from `~/.lnd`. This can be altered using command line flags. See `swapd --help`. + + `swapd` only listens on localhost and uses an unencrypted and unauthenticated connection. + +* To initiate a swap, run: + + `swapcli uncharge ` + + When the swap is initiated successfully, `swapd` will see the process through. + +* To query and track the swap status, run `swapcli` without arguments. + +## Resume +When `swapd` is terminated (or killed) for whatever reason, it will pickup pending swaps after a restart. + +Information about pending swaps is stored persistently in the swap database. Its location is `~/.swaplet//swapclient.db`. + +## Multiple simultaneous swaps + +It is possible to execute multiple swaps simultaneously. + diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..d0c644c --- /dev/null +++ b/client/client.go @@ -0,0 +1,322 @@ +package client + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/sweep" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + // ErrSwapFeeTooHigh is returned when the swap invoice amount is too + // high. + ErrSwapFeeTooHigh = errors.New("swap fee too high") + + // ErrPrepayAmountTooHigh is returned when the prepay invoice amount is + // too high. + ErrPrepayAmountTooHigh = errors.New("prepay amount too high") + + // ErrSwapAmountTooLow is returned when the requested swap amount is + // less than the server minimum. + ErrSwapAmountTooLow = errors.New("swap amount too low") + + // ErrSwapAmountTooHigh is returned when the requested swap amount is + // more than the server maximum. + ErrSwapAmountTooHigh = errors.New("swap amount too high") + + // ErrExpiryTooSoon is returned when the server proposes an expiry that + // is too soon for us. + ErrExpiryTooSoon = errors.New("swap expiry too soon") + + // ErrExpiryTooFar is returned when the server proposes an expiry that + // is too soon for us. + ErrExpiryTooFar = errors.New("swap expiry too far") + + serverRPCTimeout = 30 * time.Second + + republishDelay = 10 * time.Second +) + +// Client performs the client side part of swaps. This interface exists to +// be able to implement a stub. +type Client struct { + started uint32 // To be used atomically. + errChan chan error + + lndServices *lndclient.LndServices + sweeper *sweep.Sweeper + executor *executor + + resumeReady chan struct{} + wg sync.WaitGroup + + clientConfig +} + +// NewClient returns a new instance to initiate swaps with. +func NewClient(dbDir string, serverAddress string, insecure bool, + lnd *lndclient.LndServices) (*Client, func(), error) { + + store, err := newBoltSwapClientStore(dbDir) + if err != nil { + return nil, nil, err + } + + swapServerClient, err := newSwapServerClient(serverAddress, insecure) + if err != nil { + return nil, nil, err + } + + config := &clientConfig{ + LndServices: lnd, + Server: swapServerClient, + Store: store, + CreateExpiryTimer: func(d time.Duration) <-chan time.Time { + return time.NewTimer(d).C + }, + } + + sweeper := &sweep.Sweeper{ + Lnd: lnd, + } + + executor := newExecutor(&executorConfig{ + lnd: lnd, + store: store, + sweeper: sweeper, + createExpiryTimer: config.CreateExpiryTimer, + }) + + client := &Client{ + errChan: make(chan error), + clientConfig: *config, + lndServices: lnd, + sweeper: sweeper, + executor: executor, + resumeReady: make(chan struct{}), + } + + cleanup := func() { + swapServerClient.Close() + } + + return client, cleanup, nil +} + +// GetUnchargeSwaps returns a list of all swaps currently in the database. +func (s *Client) GetUnchargeSwaps() ([]*PersistentUncharge, error) { + return s.Store.getUnchargeSwaps() +} + +// Run is a blocking call that executes all swaps. Any pending swaps are +// restored from persistent storage and resumed. Subsequent updates +// will be sent through the passed in statusChan. The function can be +// terminated by cancelling the context. +func (s *Client) Run(ctx context.Context, + statusChan chan<- SwapInfo) error { + + if !atomic.CompareAndSwapUint32(&s.started, 0, 1) { + return errors.New("swap client can only be started once") + } + + // Log connected node. + info, err := s.lndServices.Client.GetInfo(ctx) + if err != nil { + return fmt.Errorf("GetInfo error: %v", err) + } + logger.Infof("Connected to lnd node %v with pubkey %v", + info.Alias, hex.EncodeToString(info.IdentityPubkey[:]), + ) + + // Setup main context used for cancelation. + mainCtx, mainCancel := context.WithCancel(ctx) + defer mainCancel() + + // Query store before starting event loop to prevent new swaps from + // being treated as swaps that need to be resumed. + pendingSwaps, err := s.Store.getUnchargeSwaps() + if err != nil { + return err + } + + // Start goroutine to deliver all pending swaps to the main loop. + s.wg.Add(1) + go func() { + defer s.wg.Done() + + s.resumeSwaps(mainCtx, pendingSwaps) + + // Signal that new requests can be accepted. Otherwise the new + // swap could already have been added to the store and read in + // this goroutine as being a swap that needs to be resumed. + // Resulting in two goroutines executing the same swap. + close(s.resumeReady) + }() + + // Main event loop. + err = s.executor.run(mainCtx, statusChan) + + // Consider canceled as happy flow. + if err == context.Canceled { + err = nil + } + + if err != nil { + logger.Errorf("Swap client terminating: %v", err) + } else { + logger.Info("Swap client terminating") + } + + // Cancel all remaining active goroutines. + mainCancel() + + // Wait for all to finish. + logger.Debug("Wait for executor to finish") + s.executor.waitFinished() + + logger.Debug("Wait for goroutines to finish") + s.wg.Wait() + + logger.Info("Swap client terminated") + + return err +} + +// resumeSwaps restarts all pending swaps from the provided list. +func (s *Client) resumeSwaps(ctx context.Context, + swaps []*PersistentUncharge) { + + for _, pend := range swaps { + if pend.State().Type() != StateTypePending { + continue + } + swapCfg := &swapConfig{ + lnd: s.lndServices, + store: s.Store, + } + swap, err := resumeUnchargeSwap(ctx, swapCfg, pend) + if err != nil { + logger.Errorf("resuming swap: %v", err) + continue + } + + s.executor.initiateSwap(ctx, swap) + } +} + +// Uncharge initiates a uncharge swap. It blocks until the swap is +// initiation with the swap server is completed (typically this takes +// only a short amount of time). From there on further status +// information can be acquired through the status channel returned from +// the Run call. +// +// When the call returns, the swap has been persisted and will be +// resumed automatically after restarts. +// +// The return value is a hash that uniquely identifies the new swap. +func (s *Client) Uncharge(globalCtx context.Context, + request *UnchargeRequest) (*lntypes.Hash, error) { + + logger.Infof("Uncharge %v to %v (channel: %v)", + request.Amount, request.DestAddr, + request.UnchargeChannel, + ) + + if err := s.waitForInitialized(globalCtx); err != nil { + return nil, err + } + + // Create a new swap object for this swap. + initiationHeight := s.executor.height() + swapCfg := &swapConfig{ + lnd: s.lndServices, + store: s.Store, + server: s.Server, + } + swap, err := newUnchargeSwap( + globalCtx, swapCfg, initiationHeight, request, + ) + if err != nil { + return nil, err + } + + // Post swap to the main loop. + s.executor.initiateSwap(globalCtx, swap) + + // Return hash so that the caller can identify this swap in the updates + // stream. + return &swap.hash, nil +} + +// UnchargeQuote takes a Uncharge amount and returns a break down of estimated +// costs for the client. Both the swap server and the on-chain fee estimator are +// queried to get to build the quote response. +func (s *Client) UnchargeQuote(ctx context.Context, + request *UnchargeQuoteRequest) (*UnchargeQuote, error) { + + terms, err := s.Server.GetUnchargeTerms(ctx) + if err != nil { + return nil, err + } + + if request.Amount < terms.MinSwapAmount { + return nil, ErrSwapAmountTooLow + } + + if request.Amount > terms.MaxSwapAmount { + return nil, ErrSwapAmountTooHigh + } + + logger.Infof("Offchain swap destination: %x", terms.SwapPaymentDest) + + swapFee := utils.CalcFee( + request.Amount, terms.SwapFeeBase, terms.SwapFeeRate, + ) + + minerFee, err := s.sweeper.GetSweepFee( + ctx, utils.QuoteHtlc.MaxSuccessWitnessSize, + request.SweepConfTarget, + ) + if err != nil { + return nil, err + } + + return &UnchargeQuote{ + SwapFee: swapFee, + MinerFee: minerFee, + PrepayAmount: btcutil.Amount(terms.PrepayAmt), + }, nil +} + +// UnchargeTerms returns the terms on which the server executes swaps. +func (s *Client) UnchargeTerms(ctx context.Context) ( + *UnchargeTerms, error) { + + return s.Server.GetUnchargeTerms(ctx) +} + +// waitForInitialized for swaps to be resumed and executor ready. +func (s *Client) waitForInitialized(ctx context.Context) error { + select { + case <-s.executor.ready: + case <-ctx.Done(): + return ctx.Err() + } + + select { + case <-s.resumeReady: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..d419c1a --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,291 @@ +package client + +import ( + "bytes" + "context" + "crypto/sha256" + "errors" + "testing" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/test" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + testAddr, _ = btcutil.DecodeAddress( + "rbsHiPKwAgxeo1EQYiyzJTkA8XEmWSVAKx", nil) + + testRequest = &UnchargeRequest{ + Amount: btcutil.Amount(50000), + DestAddr: testAddr, + MaxMinerFee: 50000, + SweepConfTarget: 2, + MaxSwapFee: 1050, + MaxPrepayAmount: 100, + MaxPrepayRoutingFee: 75000, + MaxSwapRoutingFee: 70000, + } + + swapInvoiceDesc = "swap" + prepayInvoiceDesc = "prepay" +) + +// TestSuccess tests the uncharge happy flow. +func TestSuccess(t *testing.T) { + defer test.Guard(t)() + + ctx := createClientTestContext(t, nil) + + // Initiate uncharge. + + hash, err := ctx.swapClient.Uncharge(context.Background(), testRequest) + if err != nil { + t.Fatal(err) + } + + ctx.assertStored() + ctx.assertStatus(StateInitiated) + + signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) + signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) + + // Expect client to register for conf + confIntent := ctx.AssertRegisterConf() + + testSuccess(ctx, testRequest.Amount, *hash, + signalPrepaymentResult, signalSwapPaymentResult, false, + confIntent, + ) +} + +// TestFailOffchain tests the handling of swap for which the server failed the +// payments. +func TestFailOffchain(t *testing.T) { + defer test.Guard(t)() + + ctx := createClientTestContext(t, nil) + + _, err := ctx.swapClient.Uncharge(context.Background(), testRequest) + if err != nil { + t.Fatal(err) + } + + ctx.assertStored() + ctx.assertStatus(StateInitiated) + + signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) + signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) + + ctx.AssertRegisterConf() + + signalSwapPaymentResult( + errors.New(lndclient.PaymentResultUnknownPaymentHash), + ) + signalPrepaymentResult( + errors.New(lndclient.PaymentResultUnknownPaymentHash), + ) + ctx.assertStatus(StateFailOffchainPayments) + + ctx.assertStoreFinished(StateFailOffchainPayments) + + ctx.finish() +} + +// TestWrongAmount asserts that the client checks the server invoice amounts. +func TestFailWrongAmount(t *testing.T) { + defer test.Guard(t)() + + test := func(t *testing.T, modifier func(*serverMock), + expectedErr error) { + + ctx := createClientTestContext(t, nil) + + // Modify mock for this subtest. + modifier(ctx.serverMock) + + _, err := ctx.swapClient.Uncharge( + context.Background(), testRequest, + ) + if err != expectedErr { + t.Fatalf("Expected %v, but got %v", expectedErr, err) + } + ctx.finish() + } + + t.Run("swap fee too high", func(t *testing.T) { + test(t, func(m *serverMock) { + m.swapInvoiceAmt += 10 + }, ErrSwapFeeTooHigh) + }) + + t.Run("prepay amount too high", func(t *testing.T) { + test(t, func(m *serverMock) { + // Keep total swap fee unchanged, but increase prepaid + // portion. + m.swapInvoiceAmt -= 10 + m.prepayInvoiceAmt += 10 + }, ErrPrepayAmountTooHigh) + }) + +} + +// TestResume tests that swaps in various states are properly resumed after a +// restart. +func TestResume(t *testing.T) { + defer test.Guard(t)() + + t.Run("not expired", func(t *testing.T) { + testResume(t, false, false, true) + }) + t.Run("expired not revealed", func(t *testing.T) { + testResume(t, true, false, false) + }) + t.Run("expired revealed", func(t *testing.T) { + testResume(t, true, true, true) + }) +} + +func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) { + defer test.Guard(t)() + + preimage := testPreimage + hash := sha256.Sum256(preimage[:]) + + dest := test.GetDestAddr(t, 0) + + amt := btcutil.Amount(50000) + + swapPayReq, err := getInvoice(hash, amt, swapInvoiceDesc) + if err != nil { + t.Fatal(err) + } + + prePayReq, err := getInvoice(hash, 100, prepayInvoiceDesc) + if err != nil { + t.Fatal(err) + } + + _, senderPubKey := test.CreateKey(1) + var senderKey [33]byte + copy(senderKey[:], senderPubKey.SerializeCompressed()) + + _, receiverPubKey := test.CreateKey(2) + var receiverKey [33]byte + copy(receiverKey[:], receiverPubKey.SerializeCompressed()) + + state := StateInitiated + if preimageRevealed { + state = StatePreimageRevealed + } + pendingSwap := &PersistentUncharge{ + Contract: &UnchargeContract{ + DestAddr: dest, + SwapInvoice: swapPayReq, + SweepConfTarget: 2, + MaxSwapRoutingFee: 70000, + SwapContract: SwapContract{ + Preimage: preimage, + AmountRequested: amt, + CltvExpiry: 744, + ReceiverKey: receiverKey, + SenderKey: senderKey, + MaxSwapFee: 60000, + PrepayInvoice: prePayReq, + MaxMinerFee: 50000, + }, + }, + Events: []*PersistentUnchargeEvent{ + { + State: state, + }, + }, + Hash: hash, + } + + if expired { + // Set cltv expiry so that it has already expired at the test + // block height. + pendingSwap.Contract.CltvExpiry = 610 + } + + ctx := createClientTestContext(t, []*PersistentUncharge{pendingSwap}) + + if preimageRevealed { + ctx.assertStatus(StatePreimageRevealed) + } else { + ctx.assertStatus(StateInitiated) + } + + signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) + signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) + + // Expect client to register for conf + confIntent := ctx.AssertRegisterConf() + + signalSwapPaymentResult(nil) + signalPrepaymentResult(nil) + + if !expectSuccess { + ctx.assertStatus(StateFailTimeout) + ctx.assertStoreFinished(StateFailTimeout) + ctx.finish() + return + } + + // Because there is no reliable payment yet, an invoice is assumed to be + // paid after resume. + + testSuccess(ctx, amt, hash, + func(r error) {}, + func(r error) {}, + preimageRevealed, + confIntent, + ) +} + +func testSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, + signalPrepaymentResult, signalSwapPaymentResult func(error), + preimageRevealed bool, confIntent *test.ConfRegistration) { + + htlcOutpoint := ctx.publishHtlc(confIntent.PkScript, amt) + + signalPrepaymentResult(nil) + + ctx.AssertRegisterSpendNtfn(confIntent.PkScript) + + // Publish tick. + ctx.expiryChan <- testTime + + if !preimageRevealed { + ctx.assertStatus(StatePreimageRevealed) + ctx.assertStorePreimageReveal() + } + + // Expect client on-chain sweep of HTLC. + sweepTx := ctx.ReceiveTx() + + if !bytes.Equal(sweepTx.TxIn[0].PreviousOutPoint.Hash[:], + htlcOutpoint.Hash[:]) { + ctx.T.Fatalf("client not sweeping from htlc tx") + } + + // Check preimage. + clientPreImage := sweepTx.TxIn[0].Witness[1] + clientPreImageHash := sha256.Sum256(clientPreImage) + if clientPreImageHash != hash { + ctx.T.Fatalf("incorrect preimage") + } + + // Simulate server pulling payment. + signalSwapPaymentResult(nil) + + ctx.NotifySpend(sweepTx, 0) + + ctx.assertStatus(StateSuccess) + + ctx.assertStoreFinished(StateSuccess) + + ctx.finish() +} diff --git a/client/config.go b/client/config.go new file mode 100644 index 0000000..5bee536 --- /dev/null +++ b/client/config.go @@ -0,0 +1,15 @@ +package client + +import ( + "time" + + "github.com/lightninglabs/nautilus/lndclient" +) + +// clientConfig contains config items for the swap client. +type clientConfig struct { + LndServices *lndclient.LndServices + Server swapServerClient + Store swapClientStore + CreateExpiryTimer func(expiry time.Duration) <-chan time.Time +} diff --git a/client/executor.go b/client/executor.go new file mode 100644 index 0000000..9afbb02 --- /dev/null +++ b/client/executor.go @@ -0,0 +1,166 @@ +package client + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/sweep" + "github.com/lightningnetwork/lnd/queue" +) + +// executorConfig contains executor configuration data. +type executorConfig struct { + lnd *lndclient.LndServices + sweeper *sweep.Sweeper + store swapClientStore + createExpiryTimer func(expiry time.Duration) <-chan time.Time +} + +// executor is responsible for executing swaps. +type executor struct { + wg sync.WaitGroup + newSwaps chan genericSwap + currentHeight uint32 + ready chan struct{} + + executorConfig +} + +// newExecutor returns a new swap executor instance. +func newExecutor(cfg *executorConfig) *executor { + return &executor{ + executorConfig: *cfg, + newSwaps: make(chan genericSwap), + ready: make(chan struct{}), + } +} + +// run starts the executor event loop. It accepts and executes new swaps, +// providing them with required config data. +func (s *executor) run(mainCtx context.Context, + statusChan chan<- SwapInfo) error { + + blockEpochChan, blockErrorChan, err := + s.lnd.ChainNotifier.RegisterBlockEpochNtfn(mainCtx) + if err != nil { + return err + } + + // Before starting, make sure we have an up to date block height. + // Otherwise we might reveal a preimage for a swap that is already + // expired. + logger.Infof("Wait for first block ntfn") + + var height int32 + setHeight := func(h int32) { + height = h + atomic.StoreUint32(&s.currentHeight, uint32(h)) + } + + select { + case h := <-blockEpochChan: + setHeight(int32(h)) + case err := <-blockErrorChan: + return err + case <-mainCtx.Done(): + return mainCtx.Err() + } + + // Start main event loop. + logger.Infof("Starting event loop at height %v", height) + + // Signal that executor being ready with an up to date block height. + close(s.ready) + + // Use a map to administer the individual notification queues for the + // swaps. + blockEpochQueues := make(map[int]*queue.ConcurrentQueue) + + // On exit, stop all queue goroutines. + defer func() { + for _, queue := range blockEpochQueues { + queue.Stop() + } + }() + + swapDoneChan := make(chan int) + nextSwapID := 0 + for { + select { + case newSwap := <-s.newSwaps: + queue := queue.NewConcurrentQueue(10) + queue.Start() + swapID := nextSwapID + blockEpochQueues[swapID] = queue + + s.wg.Add(1) + go func() { + defer s.wg.Done() + + newSwap.execute(mainCtx, &executeConfig{ + statusChan: statusChan, + sweeper: s.sweeper, + blockEpochChan: queue.ChanOut(), + timerFactory: s.executorConfig.createExpiryTimer, + }, height) + + select { + case swapDoneChan <- swapID: + case <-mainCtx.Done(): + } + }() + + nextSwapID++ + case doneID := <-swapDoneChan: + queue, ok := blockEpochQueues[doneID] + if !ok { + return fmt.Errorf( + "swap id %v not found in queues", + doneID) + } + queue.Stop() + delete(blockEpochQueues, doneID) + + case h := <-blockEpochChan: + setHeight(int32(h)) + for _, queue := range blockEpochQueues { + select { + case queue.ChanIn() <- int32(h): + case <-mainCtx.Done(): + return mainCtx.Err() + } + } + + case err := <-blockErrorChan: + return fmt.Errorf("block error: %v", err) + + case <-mainCtx.Done(): + return mainCtx.Err() + } + } +} + +// initiateSwap delivers a new swap to the executor main loop. +func (s *executor) initiateSwap(ctx context.Context, + swap genericSwap) { + + select { + case s.newSwaps <- swap: + case <-ctx.Done(): + return + } +} + +// height returns the current height known to the swap server. +func (s *executor) height() int32 { + return int32(atomic.LoadUint32(&s.currentHeight)) +} + +// waitFinished waits for all swap goroutines to finish. +func (s *executor) waitFinished() { + s.wg.Wait() +} diff --git a/client/interface.go b/client/interface.go new file mode 100644 index 0000000..f8d15fa --- /dev/null +++ b/client/interface.go @@ -0,0 +1,236 @@ +package client + +import ( + "time" + + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lntypes" +) + +// UnchargeRequest contains the required parameters for the swap. +type UnchargeRequest struct { + // Amount specifies the requested swap amount in sat. This does not + // include the swap and miner fee. + Amount btcutil.Amount + + // Destination address for the swap. + DestAddr btcutil.Address + + // MaxSwapRoutingFee is the maximum off-chain fee in msat that may be + // paid for payment to the server. This limit is applied during path + // finding. Typically this value is taken from the response of the + // UnchargeQuote call. + MaxSwapRoutingFee btcutil.Amount + + // MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be + // paid for payment to the server. This limit is applied during path + // finding. Typically this value is taken from the response of the + // UnchargeQuote call. + MaxPrepayRoutingFee btcutil.Amount + + // MaxSwapFee is the maximum we are willing to pay the server for the + // swap. This value is not disclosed in the swap initiation call, but if + // the server asks for a higher fee, we abort the swap. Typically this + // value is taken from the response of the UnchargeQuote call. It + // includes the prepay amount. + MaxSwapFee btcutil.Amount + + // MaxPrepayAmount is the maximum amount of the swap fee that may be + // charged as a prepayment. + MaxPrepayAmount btcutil.Amount + + // MaxMinerFee is the maximum in on-chain fees that we are willing to + // spent. If we want to sweep the on-chain htlc and the fee estimate + // turns out higher than this value, we cancel the swap. If the fee + // estimate is lower, we publish the sweep tx. + // + // If the sweep tx isn't confirmed, we are forced to ratchet up fees + // until it is swept. Possibly even exceeding MaxMinerFee if we get + // close to the htlc timeout. Because the initial publication revealed + // the preimage, we have no other choice. The server may already have + // pulled the off-chain htlc. Only when the fee becomes higher than the + // swap amount, we can only wait for fees to come down and hope - if we + // are past the timeout - that the server isn't publishing the + // revocation. + // + // MaxMinerFee is typically taken from the response of the + // UnchargeQuote call. + MaxMinerFee btcutil.Amount + + // SweepConfTarget specifies the targeted confirmation target for the + // client sweep tx. + SweepConfTarget int32 + + // UnchargeChannel optionally specifies the short channel id of the + // channel to uncharge. + UnchargeChannel *uint64 +} + +// UnchargeContract contains the data that is serialized to persistent storage for +// pending swaps. +type UnchargeContract struct { + SwapContract + + DestAddr btcutil.Address + + SwapInvoice string + + // MaxSwapRoutingFee is the maximum off-chain fee in msat that may be + // paid for the swap payment to the server. + MaxSwapRoutingFee btcutil.Amount + + // SweepConfTarget specifies the targeted confirmation target for the + // client sweep tx. + SweepConfTarget int32 + + // UnchargeChannel is the channel to uncharge. If zero, any channel may + // be used. + UnchargeChannel *uint64 +} + +// UnchargeSwapInfo contains status information for a uncharge swap. +type UnchargeSwapInfo struct { + UnchargeContract + + SwapInfoKit + + // State where the swap is in. + State SwapState +} + +// SwapCost is a breakdown of the final swap costs. +type SwapCost struct { + // Swap is the amount paid to the server. + Server btcutil.Amount + + // Onchain is the amount paid to miners for the onchain tx. + Onchain btcutil.Amount +} + +// UnchargeQuoteRequest specifies the swap parameters for which a quote is +// requested. +type UnchargeQuoteRequest struct { + // Amount specifies the requested swap amount in sat. This does not + // include the swap and miner fee. + Amount btcutil.Amount + + // SweepConfTarget specifies the targeted confirmation target for the + // client sweep tx. + SweepConfTarget int32 + + // TODO: Add argument to specify confirmation target for server + // publishing htlc. This may influence the swap fee quote, because the + // server needs to pay more for faster confirmations. + // + // TODO: Add arguments to specify maximum total time locks for the + // off-chain swap payment and prepayment. This may influence the + // available routes and off-chain fee estimates. To apply these maximum + // values properly, the server needs to be queried for its required + // final cltv delta values for the off-chain payments. +} + +// UnchargeQuote contains estimates for the fees making up the total swap cost +// for the client. +type UnchargeQuote struct { + // SwapFee is the fee that the swap server is charging for the swap. + SwapFee btcutil.Amount + + // PrepayAmount is the part of the swap fee that is requested as a + // prepayment. + PrepayAmount btcutil.Amount + + // MinerFee is an estimate of the on-chain fee that needs to be paid to + // sweep the htlc. + MinerFee btcutil.Amount +} + +// UnchargeTerms are the server terms on which it executes swaps. +type UnchargeTerms struct { + // SwapFeeBase is the fixed per-swap base fee. + SwapFeeBase btcutil.Amount + + // SwapFeeRate is the variable fee in parts per million. + SwapFeeRate int64 + + // PrepayAmt is the fixed part of the swap fee that needs to be prepaid. + PrepayAmt btcutil.Amount + + // MinSwapAmount is the minimum amount that the server requires for a + // swap. + MinSwapAmount btcutil.Amount + + // MaxSwapAmount is the maximum amount that the server accepts for a + // swap. + MaxSwapAmount btcutil.Amount + + // Time lock delta relative to current block height that swap server + // will accept on the swap initiation call. + CltvDelta int32 + + // SwapPaymentDest is the node pubkey where to swap payment needs to be + // sent to. + SwapPaymentDest [33]byte +} + +// SwapContract contains the base data that is serialized to persistent storage +// for pending swaps. +type SwapContract struct { + Preimage lntypes.Preimage + AmountRequested btcutil.Amount + + PrepayInvoice string + + SenderKey [33]byte + ReceiverKey [33]byte + + CltvExpiry int32 + + // MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be + // paid for the prepayment to the server. + MaxPrepayRoutingFee btcutil.Amount + + // MaxSwapFee is the maximum we are willing to pay the server for the + // swap. + MaxSwapFee btcutil.Amount + + // MaxMinerFee is the maximum in on-chain fees that we are willing to + // spend. + MaxMinerFee btcutil.Amount + + // InitiationHeight is the block height at which the swap was initiated. + InitiationHeight int32 + + // InitiationTime is the time at which the swap was initiated. + InitiationTime time.Time +} + +// SwapInfoKit contains common swap info fields. +type SwapInfoKit struct { + // Hash is the sha256 hash of the preimage that unlocks the htlcs. It is + // used to uniquely identify this swap. + Hash lntypes.Hash + + // LastUpdateTime is the time of the last update of this swap. + LastUpdateTime time.Time +} + +// SwapType indicates the type of swap. +type SwapType uint8 + +const ( + // SwapTypeCharge is a charge swap. + SwapTypeCharge SwapType = iota + + // SwapTypeUncharge is an uncharge swap. + SwapTypeUncharge +) + +// SwapInfo exposes common info fields for charge and uncharge swaps. +type SwapInfo struct { + LastUpdate time.Time + SwapHash lntypes.Hash + State SwapState + SwapType SwapType + + SwapContract +} diff --git a/client/log.go b/client/log.go new file mode 100644 index 0000000..d510225 --- /dev/null +++ b/client/log.go @@ -0,0 +1,24 @@ +package client + +import ( + "github.com/btcsuite/btclog" + "os" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var ( + backendLog = btclog.NewBackend(logWriter{}) + logger = backendLog.Logger("CLIENT") + servicesLogger = backendLog.Logger("SERVICES") +) + +// logWriter implements an io.Writer that outputs to both standard output and +// the write-end pipe of an initialized log rotator. +type logWriter struct{} + +func (logWriter) Write(p []byte) (n int, err error) { + os.Stdout.Write(p) + return len(p), nil +} diff --git a/client/server_mock_test.go b/client/server_mock_test.go new file mode 100644 index 0000000..fd6de1d --- /dev/null +++ b/client/server_mock_test.go @@ -0,0 +1,125 @@ +package client + +import ( + "context" + "errors" + + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/test" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" +) + +var ( + testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) + + testUnchargeOnChainCltvDelta = int32(30) + testCltvDelta = 50 + testSwapFeeBase = btcutil.Amount(21) + testSwapFeeRate = int64(100) + testInvoiceExpiry = 180 * time.Second + testFixedPrepayAmount = btcutil.Amount(100) + testMinSwapAmount = btcutil.Amount(10000) + testMaxSwapAmount = btcutil.Amount(1000000) + testTxConfTarget = 2 + testRepublishDelay = 10 * time.Second +) + +// serverMock is used in client unit tests to simulate swap server behaviour. +type serverMock struct { + t *testing.T + + expectedSwapAmt btcutil.Amount + swapInvoiceAmt btcutil.Amount + prepayInvoiceAmt btcutil.Amount + + height int32 + + swapInvoice string + swapHash lntypes.Hash +} + +func newServerMock() *serverMock { + return &serverMock{ + expectedSwapAmt: 50000, + + // Total swap fee: 1000 + 0.01 * 50000 = 1050 + swapInvoiceAmt: 50950, + prepayInvoiceAmt: 100, + + height: 600, + } +} + +func (s *serverMock) NewUnchargeSwap(ctx context.Context, + swapHash lntypes.Hash, amount btcutil.Amount, + receiverKey [33]byte) ( + *newUnchargeResponse, error) { + + _, senderKey := test.CreateKey(100) + + if amount != s.expectedSwapAmt { + return nil, errors.New("unexpected test swap amount") + } + + swapPayReqString, err := getInvoice(swapHash, s.swapInvoiceAmt, + swapInvoiceDesc) + if err != nil { + return nil, err + } + + prePayReqString, err := getInvoice(swapHash, s.prepayInvoiceAmt, + prepayInvoiceDesc) + if err != nil { + return nil, err + } + + var senderKeyArray [33]byte + copy(senderKeyArray[:], senderKey.SerializeCompressed()) + + return &newUnchargeResponse{ + senderKey: senderKeyArray, + swapInvoice: swapPayReqString, + prepayInvoice: prePayReqString, + expiry: s.height + testUnchargeOnChainCltvDelta, + }, nil +} + +func (s *serverMock) GetUnchargeTerms(ctx context.Context) ( + *UnchargeTerms, error) { + + dest := [33]byte{1, 2, 3} + + return &UnchargeTerms{ + SwapFeeBase: testSwapFeeBase, + SwapFeeRate: testSwapFeeRate, + SwapPaymentDest: dest, + CltvDelta: testUnchargeOnChainCltvDelta, + MinSwapAmount: testMinSwapAmount, + MaxSwapAmount: testMaxSwapAmount, + PrepayAmt: testFixedPrepayAmount, + }, nil +} + +func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, error) { + req, err := zpay32.NewInvoice( + &chaincfg.TestNet3Params, hash, testTime, + zpay32.Description(memo), + zpay32.Amount(lnwire.MilliSatoshi(1000*amt)), + ) + if err != nil { + return "", err + } + + reqString, err := test.EncodePayReq(req) + if err != nil { + return "", err + } + + return reqString, nil +} diff --git a/client/state_type.go b/client/state_type.go new file mode 100644 index 0000000..dd74753 --- /dev/null +++ b/client/state_type.go @@ -0,0 +1,17 @@ +package client + +// SwapStateType defines the types of swap states that exist. Every swap state +// defined as type SwapState above, falls into one of these SwapStateType +// categories. +type SwapStateType uint8 + +const ( + // StateTypePending indicates that the swap is still pending. + StateTypePending SwapStateType = iota + + // StateTypeSuccess indicates that the swap has completed successfully. + StateTypeSuccess + + // StateTypeFail indicates that the swap has failed. + StateTypeFail +) diff --git a/client/store.go b/client/store.go new file mode 100644 index 0000000..e9bfaee --- /dev/null +++ b/client/store.go @@ -0,0 +1,472 @@ +package client + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/coreos/bbolt" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + dbFileName = "swapclient.db" + + // unchargeSwapsBucketKey is a bucket that contains all swaps that are + // currently pending or completed. + // + // maps: swap_hash -> UnchargeContract + unchargeSwapsBucketKey = []byte("uncharge-swaps") + + // unchargeUpdatesBucketKey is a bucket that contains all updates + // pertaining to a swap. This list only ever grows. + // + // maps: update_nr -> time | state + updatesBucketKey = []byte("updates") + + // contractKey is the key that stores the serialized swap contract. + contractKey = []byte("contract") + + byteOrder = binary.BigEndian + + keyLength = 33 +) + +// boltSwapClientStore stores swap data in boltdb. +type boltSwapClientStore struct { + db *bbolt.DB +} + +// newBoltSwapClientStore creates a new client swap store. +func newBoltSwapClientStore(dbPath string) (*boltSwapClientStore, error) { + if !utils.FileExists(dbPath) { + if err := os.MkdirAll(dbPath, 0700); err != nil { + return nil, err + } + } + path := filepath.Join(dbPath, dbFileName) + bdb, err := bbolt.Open(path, 0600, nil) + if err != nil { + return nil, err + } + + err = bdb.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey) + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists(updatesBucketKey) + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists(metaBucket) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + + err = syncVersions(bdb) + if err != nil { + return nil, err + } + + return &boltSwapClientStore{ + db: bdb, + }, nil +} + +// getUnchargeSwaps returns all swaps currently in the store. +func (s *boltSwapClientStore) getUnchargeSwaps() ([]*PersistentUncharge, error) { + var swaps []*PersistentUncharge + err := s.db.View(func(tx *bbolt.Tx) error { + + bucket := tx.Bucket(unchargeSwapsBucketKey) + if bucket == nil { + return errors.New("bucket does not exist") + } + + err := bucket.ForEach(func(k, _ []byte) error { + swapBucket := bucket.Bucket(k) + if swapBucket == nil { + return fmt.Errorf("swap bucket %x not found", + k) + } + + contractBytes := swapBucket.Get(contractKey) + if contractBytes == nil { + return errors.New("contract not found") + } + + contract, err := deserializeUnchargeContract( + contractBytes, + ) + if err != nil { + return err + } + + stateBucket := swapBucket.Bucket(updatesBucketKey) + if stateBucket == nil { + return errors.New("updates bucket not found") + } + var updates []*PersistentUnchargeEvent + err = stateBucket.ForEach(func(k, v []byte) error { + event, err := deserializeUnchargeUpdate(v) + if err != nil { + return err + } + updates = append(updates, event) + return nil + }) + if err != nil { + return err + } + + var hash lntypes.Hash + copy(hash[:], k) + + swap := PersistentUncharge{ + Contract: contract, + Hash: hash, + Events: updates, + } + + swaps = append(swaps, &swap) + return nil + }) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return swaps, nil +} + +// createUncharge adds an initiated swap to the store. +func (s *boltSwapClientStore) createUncharge(hash lntypes.Hash, + swap *UnchargeContract) error { + + if hash != swap.Preimage.Hash() { + return errors.New("hash and preimage do not match") + } + + return s.db.Update(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey) + if err != nil { + return err + } + + if bucket.Get(hash[:]) != nil { + return fmt.Errorf("swap %v already exists", swap.Preimage) + } + + // Create bucket for swap. + swapBucket, err := bucket.CreateBucket(hash[:]) + if err != nil { + return err + } + + contract, err := serializeUnchargeContract(swap) + if err != nil { + return err + } + + // Store contact. + if err := swapBucket.Put(contractKey, contract); err != nil { + return err + } + + // Create empty updates bucket. + _, err = swapBucket.CreateBucket(updatesBucketKey) + return err + }) +} + +// updateUncharge stores a swap updateUncharge. +func (s *boltSwapClientStore) updateUncharge(hash lntypes.Hash, time time.Time, + state SwapState) error { + + return s.db.Update(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(unchargeSwapsBucketKey) + if bucket == nil { + return errors.New("bucket does not exist") + } + + swapBucket := bucket.Bucket(hash[:]) + if swapBucket == nil { + return errors.New("swap not found") + } + + updateBucket := swapBucket.Bucket(updatesBucketKey) + if updateBucket == nil { + return errors.New("udpate bucket not found") + } + + id, err := updateBucket.NextSequence() + if err != nil { + return err + } + + updateValue, err := serializeUnchargeUpdate(time, state) + if err != nil { + return err + } + + return updateBucket.Put(itob(id), updateValue) + }) +} + +// Close closes the underlying bolt db. +func (s *boltSwapClientStore) close() error { + return s.db.Close() +} + +func deserializeUnchargeContract(value []byte) (*UnchargeContract, error) { + r := bytes.NewReader(value) + + contract, err := deserializeContract(r) + if err != nil { + return nil, err + } + + swap := UnchargeContract{ + SwapContract: *contract, + } + + addr, err := wire.ReadVarString(r, 0) + if err != nil { + return nil, err + } + swap.DestAddr, err = btcutil.DecodeAddress(addr, nil) + if err != nil { + return nil, err + } + + swap.SwapInvoice, err = wire.ReadVarString(r, 0) + if err != nil { + return nil, err + } + + if err := binary.Read(r, byteOrder, &swap.SweepConfTarget); err != nil { + return nil, err + } + + if err := binary.Read(r, byteOrder, &swap.MaxSwapRoutingFee); err != nil { + return nil, err + } + + var unchargeChannel uint64 + if err := binary.Read(r, byteOrder, &unchargeChannel); err != nil { + return nil, err + } + if unchargeChannel != 0 { + swap.UnchargeChannel = &unchargeChannel + } + + return &swap, nil +} + +func serializeUnchargeContract(swap *UnchargeContract) ( + []byte, error) { + + var b bytes.Buffer + + serializeContract(&swap.SwapContract, &b) + + addr := swap.DestAddr.String() + if err := wire.WriteVarString(&b, 0, addr); err != nil { + return nil, err + } + + if err := wire.WriteVarString(&b, 0, swap.SwapInvoice); err != nil { + return nil, err + } + + if err := binary.Write(&b, byteOrder, swap.SweepConfTarget); err != nil { + return nil, err + } + + if err := binary.Write(&b, byteOrder, swap.MaxSwapRoutingFee); err != nil { + return nil, err + } + + var unchargeChannel uint64 + if swap.UnchargeChannel != nil { + unchargeChannel = *swap.UnchargeChannel + } + if err := binary.Write(&b, byteOrder, unchargeChannel); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +func deserializeContract(r io.Reader) (*SwapContract, error) { + swap := SwapContract{} + var err error + var unixNano int64 + if err := binary.Read(r, byteOrder, &unixNano); err != nil { + return nil, err + } + swap.InitiationTime = time.Unix(0, unixNano) + + if err := binary.Read(r, byteOrder, &swap.Preimage); err != nil { + return nil, err + } + + binary.Read(r, byteOrder, &swap.AmountRequested) + + swap.PrepayInvoice, err = wire.ReadVarString(r, 0) + if err != nil { + return nil, err + } + + n, err := r.Read(swap.SenderKey[:]) + if err != nil { + return nil, err + } + if n != keyLength { + return nil, fmt.Errorf("sender key has invalid length") + } + + n, err = r.Read(swap.ReceiverKey[:]) + if err != nil { + return nil, err + } + if n != keyLength { + return nil, fmt.Errorf("receiver key has invalid length") + } + + if err := binary.Read(r, byteOrder, &swap.CltvExpiry); err != nil { + return nil, err + } + if err := binary.Read(r, byteOrder, &swap.MaxMinerFee); err != nil { + return nil, err + } + + if err := binary.Read(r, byteOrder, &swap.MaxSwapFee); err != nil { + return nil, err + } + + if err := binary.Read(r, byteOrder, &swap.MaxPrepayRoutingFee); err != nil { + return nil, err + } + if err := binary.Read(r, byteOrder, &swap.InitiationHeight); err != nil { + return nil, err + } + + return &swap, nil +} + +func serializeContract(swap *SwapContract, b *bytes.Buffer) error { + if err := binary.Write(b, byteOrder, swap.InitiationTime.UnixNano()); err != nil { + return err + } + + if err := binary.Write(b, byteOrder, swap.Preimage); err != nil { + return err + } + + if err := binary.Write(b, byteOrder, swap.AmountRequested); err != nil { + return err + } + + if err := wire.WriteVarString(b, 0, swap.PrepayInvoice); err != nil { + return err + } + + n, err := b.Write(swap.SenderKey[:]) + if err != nil { + return err + } + if n != keyLength { + return fmt.Errorf("sender key has invalid length") + } + + n, err = b.Write(swap.ReceiverKey[:]) + if err != nil { + return err + } + if n != keyLength { + return fmt.Errorf("receiver key has invalid length") + } + + if err := binary.Write(b, byteOrder, swap.CltvExpiry); err != nil { + return err + } + + if err := binary.Write(b, byteOrder, swap.MaxMinerFee); err != nil { + return err + } + + if err := binary.Write(b, byteOrder, swap.MaxSwapFee); err != nil { + return err + } + + if err := binary.Write(b, byteOrder, swap.MaxPrepayRoutingFee); err != nil { + return err + } + + if err := binary.Write(b, byteOrder, swap.InitiationHeight); err != nil { + return err + } + + return nil +} + +func serializeUnchargeUpdate(time time.Time, state SwapState) ( + []byte, error) { + + var b bytes.Buffer + + if err := binary.Write(&b, byteOrder, time.UnixNano()); err != nil { + return nil, err + } + + if err := binary.Write(&b, byteOrder, state); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +func deserializeUnchargeUpdate(value []byte) (*PersistentUnchargeEvent, error) { + update := &PersistentUnchargeEvent{} + + r := bytes.NewReader(value) + + var unixNano int64 + if err := binary.Read(r, byteOrder, &unixNano); err != nil { + return nil, err + } + update.Time = time.Unix(0, unixNano) + + if err := binary.Read(r, byteOrder, &update.State); err != nil { + return nil, err + } + + return update, nil +} + +// itob returns an 8-byte big endian representation of v. +func itob(v uint64) []byte { + b := make([]byte, 8) + byteOrder.PutUint64(b, v) + return b +} diff --git a/client/store_interface.go b/client/store_interface.go new file mode 100644 index 0000000..1f3fce6 --- /dev/null +++ b/client/store_interface.go @@ -0,0 +1,65 @@ +package client + +import ( + "time" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// swapClientStore provides persistent storage for swaps. +type swapClientStore interface { + // getUnchargeSwaps returns all swaps currently in the store. + getUnchargeSwaps() ([]*PersistentUncharge, error) + + // createUncharge adds an initiated swap to the store. + createUncharge(hash lntypes.Hash, swap *UnchargeContract) error + + // updateUncharge stores a swap updateUncharge. + updateUncharge(hash lntypes.Hash, time time.Time, state SwapState) error +} + +// PersistentUnchargeEvent contains the dynamic data of a swap. +type PersistentUnchargeEvent struct { + State SwapState + Time time.Time +} + +// PersistentUncharge is a combination of the contract and the updates. +type PersistentUncharge struct { + Hash lntypes.Hash + + Contract *UnchargeContract + Events []*PersistentUnchargeEvent +} + +// State returns the most recent state of this swap. +func (s *PersistentUncharge) State() SwapState { + lastUpdate := s.LastUpdate() + if lastUpdate == nil { + return StateInitiated + } + + return lastUpdate.State +} + +// LastUpdate returns the most recent update of this swap. +func (s *PersistentUncharge) LastUpdate() *PersistentUnchargeEvent { + eventCount := len(s.Events) + + if eventCount == 0 { + return nil + } + + lastEvent := s.Events[eventCount-1] + return lastEvent +} + +// LastUpdateTime returns the last update time of this swap. +func (s *PersistentUncharge) LastUpdateTime() time.Time { + lastUpdate := s.LastUpdate() + if lastUpdate == nil { + return s.Contract.InitiationTime + } + + return lastUpdate.Time +} diff --git a/client/store_meta.go b/client/store_meta.go new file mode 100644 index 0000000..658a5a3 --- /dev/null +++ b/client/store_meta.go @@ -0,0 +1,123 @@ +package client + +import ( + "errors" + "fmt" + + "github.com/coreos/bbolt" +) + +var ( + // metaBucket stores all the meta information concerning the state of + // the database. + metaBucket = []byte("metadata") + + // dbVersionKey is a boltdb key and it's used for storing/retrieving + // current database version. + dbVersionKey = []byte("dbp") + + // ErrDBReversion is returned when detecting an attempt to revert to a + // prior database version. + ErrDBReversion = fmt.Errorf("channel db cannot revert to prior version") +) + +// migration is a function which takes a prior outdated version of the database +// instances and mutates the key/bucket structure to arrive at a more +// up-to-date version of the database. +type migration func(tx *bbolt.Tx) error + +var ( + // dbVersions is storing all versions of database. If current version + // of database don't match with latest version this list will be used + // for retrieving all migration function that are need to apply to the + // current db. + migrations = []migration{} + + latestDBVersion = uint32(len(migrations)) +) + +// getDBVersion retrieves the current db version. +func getDBVersion(db *bbolt.DB) (uint32, error) { + var version uint32 + + err := db.View(func(tx *bbolt.Tx) error { + metaBucket := tx.Bucket(metaBucket) + if metaBucket == nil { + return errors.New("bucket does not exist") + } + + data := metaBucket.Get(dbVersionKey) + // If no version key found, assume version is 0. + if data != nil { + version = byteOrder.Uint32(data) + } + + return nil + }) + if err != nil { + return 0, err + } + + return version, nil +} + +// getDBVersion updates the current db version. +func setDBVersion(tx *bbolt.Tx, version uint32) error { + metaBucket := tx.Bucket(metaBucket) + if metaBucket == nil { + return errors.New("bucket does not exist") + } + + scratch := make([]byte, 4) + byteOrder.PutUint32(scratch, version) + return metaBucket.Put(dbVersionKey, scratch) +} + +// syncVersions function is used for safe db version synchronization. It +// applies migration functions to the current database and recovers the +// previous state of db if at least one error/panic appeared during migration. +func syncVersions(db *bbolt.DB) error { + currentVersion, err := getDBVersion(db) + if err != nil { + return err + } + + logger.Infof("Checking for schema update: latest_version=%v, "+ + "db_version=%v", latestDBVersion, currentVersion) + + switch { + + // If the database reports a higher version that we are aware of, the + // user is probably trying to revert to a prior version of lnd. We fail + // here to prevent reversions and unintended corruption. + case currentVersion > latestDBVersion: + logger.Errorf("Refusing to revert from db_version=%d to "+ + "lower version=%d", currentVersion, + latestDBVersion) + + return ErrDBReversion + + // If the current database version matches the latest version number, + // then we don't need to perform any migrations. + case currentVersion == latestDBVersion: + return nil + } + + logger.Infof("Performing database schema migration") + + // Otherwise we execute the migrations serially within a single database + // transaction to ensure the migration is atomic. + return db.Update(func(tx *bbolt.Tx) error { + for v := currentVersion; v < latestDBVersion; v++ { + logger.Infof("Applying migration #%v", v+1) + migration := migrations[v] + if err := migration(tx); err != nil { + logger.Infof("Unable to apply migration #%v", + v+1) + return err + } + } + + return setDBVersion(tx, latestDBVersion) + }) +} diff --git a/client/store_mock_test.go b/client/store_mock_test.go new file mode 100644 index 0000000..c16ed54 --- /dev/null +++ b/client/store_mock_test.go @@ -0,0 +1,146 @@ +package client + +import ( + "errors" + "testing" + "time" + + "github.com/lightninglabs/nautilus/test" + "github.com/lightningnetwork/lnd/lntypes" +) + +// storeMock implements a mock client swap store. +type storeMock struct { + unchargeSwaps map[lntypes.Hash]*UnchargeContract + unchargeUpdates map[lntypes.Hash][]SwapState + unchargeStoreChan chan UnchargeContract + unchargeUpdateChan chan SwapState + + t *testing.T +} + +type finishData struct { + preimage lntypes.Hash + result SwapState +} + +// NewStoreMock instantiates a new mock store. +func newStoreMock(t *testing.T) *storeMock { + return &storeMock{ + unchargeStoreChan: make(chan UnchargeContract, 1), + unchargeUpdateChan: make(chan SwapState, 1), + unchargeSwaps: make(map[lntypes.Hash]*UnchargeContract), + unchargeUpdates: make(map[lntypes.Hash][]SwapState), + + t: t, + } +} + +// getUnchargeSwaps returns all swaps currently in the store. +func (s *storeMock) getUnchargeSwaps() ([]*PersistentUncharge, error) { + result := []*PersistentUncharge{} + + for hash, contract := range s.unchargeSwaps { + updates := s.unchargeUpdates[hash] + events := make([]*PersistentUnchargeEvent, len(updates)) + for i, u := range updates { + events[i] = &PersistentUnchargeEvent{ + State: u, + } + } + + swap := &PersistentUncharge{ + Hash: hash, + Contract: contract, + Events: events, + } + result = append(result, swap) + } + + return result, nil +} + +// createUncharge adds an initiated swap to the store. +func (s *storeMock) createUncharge(hash lntypes.Hash, + swap *UnchargeContract) error { + + _, ok := s.unchargeSwaps[hash] + if ok { + return errors.New("swap already exists") + } + + s.unchargeSwaps[hash] = swap + s.unchargeUpdates[hash] = []SwapState{} + s.unchargeStoreChan <- *swap + + return nil +} + +// Finalize stores the final swap result. +func (s *storeMock) updateUncharge(hash lntypes.Hash, time time.Time, + state SwapState) error { + + updates, ok := s.unchargeUpdates[hash] + if !ok { + return errors.New("swap does not exists") + } + + updates = append(updates, state) + s.unchargeUpdates[hash] = updates + s.unchargeUpdateChan <- state + + return nil +} + +func (s *storeMock) isDone() error { + select { + case <-s.unchargeStoreChan: + return errors.New("storeChan not empty") + default: + } + + select { + case <-s.unchargeUpdateChan: + return errors.New("updateChan not empty") + default: + } + return nil +} + +func (s *storeMock) assertUnchargeStored() { + s.t.Helper() + + select { + case <-s.unchargeStoreChan: + case <-time.After(test.Timeout): + s.t.Fatalf("expected swap to be stored") + } +} + +func (s *storeMock) assertStorePreimageReveal() { + + s.t.Helper() + + select { + case state := <-s.unchargeUpdateChan: + if state != StatePreimageRevealed { + s.t.Fatalf("unexpected state") + } + case <-time.After(test.Timeout): + s.t.Fatalf("expected swap to be marked as preimage revealed") + } +} + +func (s *storeMock) assertStoreFinished(expectedResult SwapState) { + s.t.Helper() + + select { + case state := <-s.unchargeUpdateChan: + if state != expectedResult { + s.t.Fatalf("expected result %v, but got %v", + expectedResult, state) + } + case <-time.After(test.Timeout): + s.t.Fatalf("expected swap to be finished") + } +} diff --git a/client/store_test.go b/client/store_test.go new file mode 100644 index 0000000..ffa4f87 --- /dev/null +++ b/client/store_test.go @@ -0,0 +1,131 @@ +package client + +import ( + "crypto/sha256" + "io/ioutil" + "reflect" + "testing" + "time" + + "github.com/lightninglabs/nautilus/test" +) + +func TestStore(t *testing.T) { + + tempDirName, err := ioutil.TempDir("", "clientstore") + if err != nil { + t.Fatal(err) + } + + store, err := newBoltSwapClientStore(tempDirName) + if err != nil { + t.Fatal(err) + } + + swaps, err := store.getUnchargeSwaps() + if err != nil { + t.Fatal(err) + } + + if len(swaps) != 0 { + t.Fatal("expected empty store") + } + + destAddr := test.GetDestAddr(t, 0) + + senderKey := [33]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2} + + receiverKey := [33]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3} + + hash := sha256.Sum256(testPreimage[:]) + + initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC) + + pendingSwap := UnchargeContract{ + SwapContract: SwapContract{ + AmountRequested: 100, + Preimage: testPreimage, + CltvExpiry: 144, + SenderKey: senderKey, + PrepayInvoice: "prepayinvoice", + ReceiverKey: receiverKey, + MaxMinerFee: 10, + MaxSwapFee: 20, + MaxPrepayRoutingFee: 40, + InitiationHeight: 99, + + // Convert to/from unix to remove timezone, so that it + // doesn't interfere with DeepEqual. + InitiationTime: time.Unix(0, initiationTime.UnixNano()), + }, + DestAddr: destAddr, + SwapInvoice: "swapinvoice", + MaxSwapRoutingFee: 30, + SweepConfTarget: 2, + } + + checkSwap := func(expectedState SwapState) { + t.Helper() + + swaps, err := store.getUnchargeSwaps() + if err != nil { + t.Fatal(err) + } + + if len(swaps) != 1 { + t.Fatal("expected pending swap in store") + } + + swap := swaps[0].Contract + if !reflect.DeepEqual(swap, &pendingSwap) { + t.Fatal("invalid pending swap data") + } + + if swaps[0].State() != expectedState { + t.Fatalf("expected state %v, but got %v", + expectedState, swaps[0].State(), + ) + } + } + + err = store.createUncharge(hash, &pendingSwap) + if err != nil { + t.Fatal(err) + } + + checkSwap(StateInitiated) + + err = store.createUncharge(hash, &pendingSwap) + if err == nil { + t.Fatal("expected error on storing duplicate") + } + + checkSwap(StateInitiated) + + if err := store.updateUncharge(hash, testTime, StatePreimageRevealed); err != nil { + t.Fatal(err) + } + + checkSwap(StatePreimageRevealed) + + if err := store.updateUncharge(hash, testTime, StateFailInsufficientValue); err != nil { + t.Fatal(err) + } + + checkSwap(StateFailInsufficientValue) + + err = store.close() + if err != nil { + t.Fatal(err) + } + + // Reopen store + store, err = newBoltSwapClientStore(tempDirName) + if err != nil { + t.Fatal(err) + } + + checkSwap(StateFailInsufficientValue) +} diff --git a/client/swap.go b/client/swap.go new file mode 100644 index 0000000..f20317a --- /dev/null +++ b/client/swap.go @@ -0,0 +1,96 @@ +package client + +import ( + "context" + "time" + + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/lntypes" +) + +type swapKit struct { + htlc *utils.Htlc + hash lntypes.Hash + + height int32 + + log *utils.SwapLog + + lastUpdateTime time.Time + cost SwapCost + state SwapState + executeConfig + swapConfig + + contract *SwapContract + swapType SwapType +} + +func newSwapKit(hash lntypes.Hash, swapType SwapType, cfg *swapConfig, + contract *SwapContract) (*swapKit, error) { + + // Compose expected on-chain swap script + htlc, err := utils.NewHtlc( + contract.CltvExpiry, contract.SenderKey, + contract.ReceiverKey, hash, + ) + if err != nil { + return nil, err + } + + // Log htlc address for debugging. + htlcAddress, err := htlc.Address(cfg.lnd.ChainParams) + if err != nil { + return nil, err + } + + log := &utils.SwapLog{ + Hash: hash, + Logger: logger, + } + + log.Infof("Htlc address: %v", htlcAddress) + + return &swapKit{ + swapConfig: *cfg, + hash: hash, + log: log, + htlc: htlc, + state: StateInitiated, + contract: contract, + swapType: swapType, + }, nil +} + +// sendUpdate reports an update to the swap state. +func (s *swapKit) sendUpdate(ctx context.Context) error { + info := &SwapInfo{ + SwapContract: *s.contract, + SwapHash: s.hash, + SwapType: s.swapType, + LastUpdate: s.lastUpdateTime, + State: s.state, + } + + s.log.Infof("state %v", info.State) + + select { + case s.statusChan <- *info: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} + +type genericSwap interface { + execute(mainCtx context.Context, cfg *executeConfig, + height int32) error +} + +type swapConfig struct { + lnd *lndclient.LndServices + store swapClientStore + server swapServerClient +} diff --git a/client/swap_server_client.go b/client/swap_server_client.go new file mode 100644 index 0000000..8ff62f8 --- /dev/null +++ b/client/swap_server_client.go @@ -0,0 +1,143 @@ +package client + +import ( + "context" + "crypto/tls" + "encoding/hex" + "errors" + "fmt" + + "github.com/lightningnetwork/lnd/lntypes" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/rpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type swapServerClient interface { + GetUnchargeTerms(ctx context.Context) ( + *UnchargeTerms, error) + + NewUnchargeSwap(ctx context.Context, + swapHash lntypes.Hash, amount btcutil.Amount, + receiverKey [33]byte) ( + *newUnchargeResponse, error) +} + +type grpcSwapServerClient struct { + server rpc.SwapServerClient + conn *grpc.ClientConn +} + +func newSwapServerClient(address string, insecure bool) (*grpcSwapServerClient, error) { + serverConn, err := getSwapServerConn(address, insecure) + if err != nil { + return nil, err + } + + server := rpc.NewSwapServerClient(serverConn) + + return &grpcSwapServerClient{ + conn: serverConn, + server: server, + }, nil +} + +func (s *grpcSwapServerClient) GetUnchargeTerms(ctx context.Context) ( + *UnchargeTerms, error) { + + rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + defer rpcCancel() + quoteResp, err := s.server.UnchargeQuote(rpcCtx, + &rpc.ServerUnchargeQuoteRequest{}, + ) + if err != nil { + return nil, err + } + + dest, err := hex.DecodeString(quoteResp.SwapPaymentDest) + if err != nil { + return nil, err + } + if len(dest) != 33 { + return nil, errors.New("invalid payment dest") + } + var destArray [33]byte + copy(destArray[:], dest) + + return &UnchargeTerms{ + MinSwapAmount: btcutil.Amount(quoteResp.MinSwapAmount), + MaxSwapAmount: btcutil.Amount(quoteResp.MaxSwapAmount), + PrepayAmt: btcutil.Amount(quoteResp.PrepayAmt), + SwapFeeBase: btcutil.Amount(quoteResp.SwapFeeBase), + SwapFeeRate: quoteResp.SwapFeeRate, + CltvDelta: quoteResp.CltvDelta, + SwapPaymentDest: destArray, + }, nil +} + +func (s *grpcSwapServerClient) NewUnchargeSwap(ctx context.Context, + swapHash lntypes.Hash, amount btcutil.Amount, receiverKey [33]byte) ( + *newUnchargeResponse, error) { + + rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + defer rpcCancel() + swapResp, err := s.server.NewUnchargeSwap(rpcCtx, + &rpc.ServerUnchargeSwapRequest{ + SwapHash: swapHash[:], + Amt: uint64(amount), + ReceiverKey: receiverKey[:], + }, + ) + if err != nil { + return nil, err + } + + var senderKey [33]byte + copy(senderKey[:], swapResp.SenderKey) + + // Validate sender key. + _, err = btcec.ParsePubKey(senderKey[:], btcec.S256()) + if err != nil { + return nil, fmt.Errorf("invalid sender key: %v", err) + } + + return &newUnchargeResponse{ + swapInvoice: swapResp.SwapInvoice, + prepayInvoice: swapResp.PrepayInvoice, + senderKey: senderKey, + expiry: swapResp.Expiry, + }, nil +} + +func (s *grpcSwapServerClient) Close() { + s.conn.Close() +} + +// getSwapServerConn returns a connection to the swap server. +func getSwapServerConn(address string, insecure bool) (*grpc.ClientConn, error) { + // Create a dial options array. + opts := []grpc.DialOption{} + if insecure { + opts = append(opts, grpc.WithInsecure()) + } else { + creds := credentials.NewTLS(&tls.Config{}) + opts = append(opts, grpc.WithTransportCredentials(creds)) + } + + conn, err := grpc.Dial(address, opts...) + if err != nil { + return nil, fmt.Errorf("unable to connect to RPC server: %v", err) + } + + return conn, nil +} + +type newUnchargeResponse struct { + swapInvoice string + prepayInvoice string + senderKey [33]byte + expiry int32 +} diff --git a/client/testcontext_test.go b/client/testcontext_test.go new file mode 100644 index 0000000..97c5aed --- /dev/null +++ b/client/testcontext_test.go @@ -0,0 +1,234 @@ +package client + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/sweep" + "github.com/lightninglabs/nautilus/test" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + testPreimage = lntypes.Preimage([32]byte{ + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, + }) + testPrepayPreimage = lntypes.Preimage([32]byte{ + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 5, + }) + + testStartingHeight = uint32(600) +) + +// testContext contains functionality to support client unit tests. +type testContext struct { + test.Context + + serverMock *serverMock + swapClient *Client + statusChan chan SwapInfo + store *storeMock + expiryChan chan time.Time + runErr chan error + stop func() +} + +func newSwapClient(config *clientConfig) *Client { + sweeper := &sweep.Sweeper{ + Lnd: config.LndServices, + } + + lndServices := config.LndServices + + executor := newExecutor(&executorConfig{ + lnd: lndServices, + store: config.Store, + sweeper: sweeper, + createExpiryTimer: config.CreateExpiryTimer, + }) + + return &Client{ + errChan: make(chan error), + clientConfig: *config, + lndServices: lndServices, + sweeper: sweeper, + executor: executor, + resumeReady: make(chan struct{}), + } +} + +func createClientTestContext(t *testing.T, + pendingSwaps []*PersistentUncharge) *testContext { + + serverMock := newServerMock() + + clientLnd := test.NewMockLnd() + + store := newStoreMock(t) + for _, s := range pendingSwaps { + store.unchargeSwaps[s.Hash] = s.Contract + + updates := []SwapState{} + for _, e := range s.Events { + updates = append(updates, e.State) + } + store.unchargeUpdates[s.Hash] = updates + } + + expiryChan := make(chan time.Time) + timerFactory := func(expiry time.Duration) <-chan time.Time { + return expiryChan + } + + swapClient := newSwapClient(&clientConfig{ + LndServices: &clientLnd.LndServices, + Server: serverMock, + Store: store, + CreateExpiryTimer: timerFactory, + }) + + statusChan := make(chan SwapInfo) + + ctx := &testContext{ + Context: test.NewContext( + t, + clientLnd, + ), + swapClient: swapClient, + statusChan: statusChan, + expiryChan: expiryChan, + store: store, + serverMock: serverMock, + } + + ctx.runErr = make(chan error) + runCtx, stop := context.WithCancel(context.Background()) + ctx.stop = stop + + go func() { + ctx.runErr <- swapClient.Run( + runCtx, + statusChan, + ) + }() + + return ctx +} + +func (ctx *testContext) finish() { + ctx.stop() + select { + case err := <-ctx.runErr: + if err != nil { + ctx.T.Fatal(err) + } + case <-time.After(test.Timeout): + ctx.T.Fatal("client not stopping") + } + + ctx.assertIsDone() +} + +// notifyHeight notifies swap client of the arrival of a new block and waits for +// the notification to be processed by selecting on a dedicated test channel. +func (ctx *testContext) notifyHeight(height int32) { + ctx.T.Helper() + + if err := ctx.Lnd.NotifyHeight(height); err != nil { + ctx.T.Fatal(err) + } +} + +func (ctx *testContext) assertIsDone() { + if err := ctx.Lnd.IsDone(); err != nil { + ctx.T.Fatal(err) + } + + if err := ctx.store.isDone(); err != nil { + ctx.T.Fatal(err) + } + + select { + case <-ctx.statusChan: + ctx.T.Fatalf("not all status updates read") + default: + } +} + +func (ctx *testContext) assertStored() { + ctx.T.Helper() + + ctx.store.assertUnchargeStored() +} + +func (ctx *testContext) assertStorePreimageReveal() { + ctx.T.Helper() + + ctx.store.assertStorePreimageReveal() +} + +func (ctx *testContext) assertStoreFinished(expectedResult SwapState) { + ctx.T.Helper() + + ctx.store.assertStoreFinished(expectedResult) + +} + +func (ctx *testContext) assertStatus(expectedState SwapState) { + + ctx.T.Helper() + + for { + select { + case update := <-ctx.statusChan: + if update.SwapType != SwapTypeUncharge { + continue + } + + if update.State == expectedState { + return + } + case <-time.After(test.Timeout): + ctx.T.Fatalf("expected status %v not "+ + "received in time", expectedState) + } + } +} + +func (ctx *testContext) publishHtlc(script []byte, amt btcutil.Amount) wire.OutPoint { + // Create the htlc tx. + htlcTx := wire.MsgTx{} + htlcTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{}, + }) + htlcTx.AddTxOut(&wire.TxOut{ + PkScript: script, + Value: int64(amt), + }) + + htlcTxHash := htlcTx.TxHash() + + // Signal client that script has been published. + select { + case ctx.Lnd.ConfChannel <- &chainntnfs.TxConfirmation{ + Tx: &htlcTx, + }: + case <-time.After(test.Timeout): + ctx.T.Fatalf("htlc confirmed not consumed") + } + + return wire.OutPoint{ + Hash: htlcTxHash, + Index: 0, + } +} diff --git a/client/uncharge.go b/client/uncharge.go new file mode 100644 index 0000000..e851f76 --- /dev/null +++ b/client/uncharge.go @@ -0,0 +1,675 @@ +package client + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "fmt" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/sweep" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + // MinUnchargePreimageRevealDelta configures the minimum number of remaining + // blocks before htlc expiry required to reveal preimage. + MinUnchargePreimageRevealDelta = int32(20) +) + +// unchargeSwap contains all the in-memory state related to a pending uncharge +// swap. +type unchargeSwap struct { + swapKit + + UnchargeContract + + swapPaymentChan chan lndclient.PaymentResult + prePaymentChan chan lndclient.PaymentResult +} + +// executeConfig contains extra configuration to execute the swap. +type executeConfig struct { + sweeper *sweep.Sweeper + statusChan chan<- SwapInfo + blockEpochChan <-chan interface{} + timerFactory func(d time.Duration) <-chan time.Time +} + +// newUnchargeSwap initiates a new swap with the server and returns a +// corresponding swap object. +func newUnchargeSwap(globalCtx context.Context, cfg *swapConfig, + currentHeight int32, request *UnchargeRequest) (*unchargeSwap, error) { + + // Generate random preimage. + var swapPreimage [32]byte + if _, err := rand.Read(swapPreimage[:]); err != nil { + logger.Error("Cannot generate preimage") + } + swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:])) + + // Derive a receiver key for this swap. + keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey( + globalCtx, utils.SwapKeyFamily, + ) + if err != nil { + return nil, err + } + var receiverKey [33]byte + copy(receiverKey[:], keyDesc.PubKey.SerializeCompressed()) + + // Post the swap parameters to the swap server. The response contains + // the server revocation key and the swap and prepay invoices. + logger.Infof("Initiating swap request at height %v", currentHeight) + + swapResp, err := cfg.server.NewUnchargeSwap(globalCtx, swapHash, + request.Amount, receiverKey, + ) + if err != nil { + return nil, fmt.Errorf("cannot initiate swap: %v", err) + } + + err = validateUnchargeContract(cfg.lnd, currentHeight, request, swapResp) + if err != nil { + return nil, err + } + + // Instantie a struct that contains all required data to start the swap. + initiationTime := time.Now() + + contract := UnchargeContract{ + SwapInvoice: swapResp.swapInvoice, + DestAddr: request.DestAddr, + MaxSwapRoutingFee: request.MaxSwapRoutingFee, + SweepConfTarget: request.SweepConfTarget, + UnchargeChannel: request.UnchargeChannel, + SwapContract: SwapContract{ + InitiationHeight: currentHeight, + InitiationTime: initiationTime, + PrepayInvoice: swapResp.prepayInvoice, + ReceiverKey: receiverKey, + SenderKey: swapResp.senderKey, + Preimage: swapPreimage, + AmountRequested: request.Amount, + CltvExpiry: swapResp.expiry, + MaxMinerFee: request.MaxMinerFee, + MaxSwapFee: request.MaxSwapFee, + MaxPrepayRoutingFee: request.MaxPrepayRoutingFee, + }, + } + + swapKit, err := newSwapKit( + swapHash, SwapTypeUncharge, cfg, &contract.SwapContract, + ) + if err != nil { + return nil, err + } + + swapKit.lastUpdateTime = initiationTime + + swap := &unchargeSwap{ + UnchargeContract: contract, + swapKit: *swapKit, + } + + // Persist the data before exiting this function, so that the caller can + // trust that this swap will be resumed on restart. + err = cfg.store.createUncharge(swapHash, &swap.UnchargeContract) + if err != nil { + return nil, fmt.Errorf("cannot store swap: %v", err) + } + + return swap, nil +} + +// resumeUnchargeSwap returns a swap object representing a pending swap that has +// been restored from the database. +func resumeUnchargeSwap(reqContext context.Context, cfg *swapConfig, + pend *PersistentUncharge) (*unchargeSwap, error) { + + hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:])) + + logger.Infof("Resuming swap %v", hash) + + swapKit, err := newSwapKit( + hash, SwapTypeUncharge, cfg, &pend.Contract.SwapContract, + ) + if err != nil { + return nil, err + } + + swap := &unchargeSwap{ + UnchargeContract: *pend.Contract, + swapKit: *swapKit, + } + + lastUpdate := pend.LastUpdate() + if lastUpdate == nil { + swap.lastUpdateTime = pend.Contract.InitiationTime + } else { + swap.state = lastUpdate.State + swap.lastUpdateTime = lastUpdate.Time + } + + return swap, nil +} + +// execute starts/resumes the swap. It is a thin wrapper around +// executeAndFinalize to conveniently handle the error case. +func (s *unchargeSwap) execute(mainCtx context.Context, + cfg *executeConfig, height int32) error { + + s.executeConfig = *cfg + s.height = height + + err := s.executeAndFinalize(mainCtx) + + // If an unexpected error happened, report a temporary failure. + // Otherwise for example a connection error could lead to abandoning the + // swap permanently and losing funds. + if err != nil { + s.log.Errorf("Swap error: %v", err) + s.state = StateFailTemporary + + // If we cannot send out this update, there is nothing we can do. + _ = s.sendUpdate(mainCtx) + } + + return err +} + +// executeAndFinalize executes a swap and awaits the definitive outcome of the +// offchain payments. When this method returns, the swap outcome is final. +func (s *unchargeSwap) executeAndFinalize(globalCtx context.Context) error { + // Announce swap by sending out an initial update. + err := s.sendUpdate(globalCtx) + if err != nil { + return err + } + + // Execute swap. When this call returns, the swap outcome is final, but + // it may be that there are still off-chain payments pending. + err = s.executeSwap(globalCtx) + if err != nil { + return err + } + + // Sanity check. + if s.state.Type() == StateTypePending { + return fmt.Errorf("swap in non-final state %v", s.state) + } + + // Wait until all offchain payments have completed. If payments have + // already completed early, their channels have been set to nil. + s.log.Infof("Wait for server pulling off-chain payment(s)") + for s.swapPaymentChan != nil || s.prePaymentChan != nil { + select { + case result := <-s.swapPaymentChan: + s.swapPaymentChan = nil + if result.Err != nil { + // Server didn't pull the swap payment. + s.log.Infof("Swap payment failed: %v", + result.Err) + + continue + } + s.cost.Server += result.PaidAmt + + case result := <-s.prePaymentChan: + s.prePaymentChan = nil + if result.Err != nil { + // Server didn't pull the prepayment. + s.log.Infof("Prepayment failed: %v", + result.Err) + + continue + } + s.cost.Server += result.PaidAmt + + case <-globalCtx.Done(): + return globalCtx.Err() + } + } + + // Mark swap completed in store. + s.log.Infof("Swap completed: %v "+ + "(final cost: server %v, onchain %v)", + s.state, + s.cost.Server, + s.cost.Onchain, + ) + + return s.persistState(globalCtx) +} + +// executeSwap executes the swap, but returns as soon as the swap outcome is +// final. At that point, there may still be pending off-chain payment(s). +func (s *unchargeSwap) executeSwap(globalCtx context.Context) error { + // We always pay both invoices (again). This is currently the only way + // to sort of resume payments. + // + // TODO: We shouldn't pay the invoices if it is already too late to + // start the swap. But because we don't know if we already fired the + // payments in a previous run, we cannot just abandon here. + s.payInvoices(globalCtx) + + // Wait for confirmation of the on-chain htlc by watching for a tx + // producing the swap script output. + txConf, err := s.waitForConfirmedHtlc(globalCtx) + if err != nil { + return err + } + + // If no error and no confirmation, the swap is aborted without an + // error. The swap state has been updated to a final state. + if txConf == nil { + return nil + } + + // TODO: Off-chain payments can be canceled here. Most probably the HTLC + // is accepted by the server, but in case there are not for whatever + // reason, we don't need to have mission control start another payment + // attempt. + + // Retrieve outpoint for sweep. + htlcOutpoint, htlcValue, err := utils.GetScriptOutput( + txConf.Tx, s.htlc.ScriptHash, + ) + if err != nil { + return err + } + + s.log.Infof("Htlc value: %v", htlcValue) + + // Verify amount if preimage hasn't been revealed yet. + if s.state != StatePreimageRevealed && htlcValue < s.AmountRequested { + logger.Warnf("Swap amount too low, expected %v but received %v", + s.AmountRequested, htlcValue) + + s.state = StateFailInsufficientValue + return nil + } + + // Try to spend htlc and continue (rbf) until a spend has confirmed. + spendDetails, err := s.waitForHtlcSpendConfirmed(globalCtx, + func() error { + return s.sweep(globalCtx, *htlcOutpoint, htlcValue) + }, + ) + if err != nil { + return err + } + + // Inspect witness stack to see if it is a success transaction. We don't + // just try to match with the hash of our sweep tx, because it may be + // swept by a different (fee) sweep tx from a previous run. + + htlcInput, err := getTxInputByOutpoint( + spendDetails.SpendingTx, htlcOutpoint, + ) + if err != nil { + return err + } + + sweepSuccessful := s.htlc.IsSuccessWitness(htlcInput.Witness) + if sweepSuccessful { + s.cost.Server -= htlcValue + + s.cost.Onchain = htlcValue - + btcutil.Amount(spendDetails.SpendingTx.TxOut[0].Value) + + s.state = StateSuccess + } else { + s.state = StateFailSweepTimeout + } + + return nil +} + +// persistState updates the swap state and sends out an update notification. +func (s *unchargeSwap) persistState(ctx context.Context) error { + updateTime := time.Now() + + s.lastUpdateTime = updateTime + + // Update state in store. + err := s.store.updateUncharge(s.hash, updateTime, s.state) + if err != nil { + return err + } + + // Send out swap update + return s.sendUpdate(ctx) +} + +// payInvoices pays both swap invoices. +func (s *unchargeSwap) payInvoices(ctx context.Context) { + // Pay the swap invoice. + s.log.Infof("Sending swap payment %v", s.SwapInvoice) + s.swapPaymentChan = s.lnd.Client.PayInvoice( + ctx, s.SwapInvoice, s.MaxSwapRoutingFee, + s.UnchargeContract.UnchargeChannel, + ) + + // Pay the prepay invoice. + s.log.Infof("Sending prepayment %v", s.PrepayInvoice) + s.prePaymentChan = s.lnd.Client.PayInvoice( + ctx, s.PrepayInvoice, s.MaxPrepayRoutingFee, + nil, + ) +} + +// waitForConfirmedHtlc waits for a confirmed htlc to appear on the chain. In +// case we haven't revealed the preimage yet, it also monitors block height and +// off-chain payment failure. +func (s *unchargeSwap) waitForConfirmedHtlc(globalCtx context.Context) ( + *chainntnfs.TxConfirmation, error) { + + // Wait for confirmation of the on-chain htlc by watching for a tx + // producing the swap script output. + s.log.Infof( + "Register conf ntfn for swap script on chain (hh=%v)", + s.InitiationHeight, + ) + + ctx, cancel := context.WithCancel(globalCtx) + defer cancel() + htlcConfChan, htlcErrChan, err := + s.lnd.ChainNotifier.RegisterConfirmationsNtfn( + ctx, nil, s.htlc.ScriptHash, 1, + s.InitiationHeight, + ) + if err != nil { + return nil, err + } + + var txConf *chainntnfs.TxConfirmation + if s.state == StateInitiated { + // Check if it is already too late to start this swap. If we + // already revealed the preimage, this check is irrelevant and + // we need to sweep in any case. + maxPreimageRevealHeight := s.CltvExpiry - + MinUnchargePreimageRevealDelta + + checkMaxRevealHeightExceeded := func() bool { + s.log.Infof("Checking preimage reveal height %v "+ + "exceeded (height %v)", + maxPreimageRevealHeight, s.height) + + if s.height <= maxPreimageRevealHeight { + return false + } + + s.log.Infof("Max preimage reveal height %v "+ + "exceeded (height %v)", + maxPreimageRevealHeight, s.height) + + s.state = StateFailTimeout + + return true + } + + // First check, because after resume we may otherwise reveal the + // preimage after the max height (depending on order in which + // events are received in the select loop below). + if checkMaxRevealHeightExceeded() { + return nil, nil + } + s.log.Infof("Waiting for either htlc on-chain confirmation or " + + " off-chain payment failure") + loop: + for { + select { + // If the swap payment fails, abandon the swap. We may + // have lost the prepayment. + case result := <-s.swapPaymentChan: + s.swapPaymentChan = nil + if result.Err != nil { + s.state = StateFailOffchainPayments + s.log.Infof("Failed swap payment: %v", + result.Err) + + return nil, nil + } + s.cost.Server += result.PaidAmt + + // If the prepay fails, abandon the swap. Because we + // didn't reveal the preimage, the swap payment will be + // canceled or time out. + case result := <-s.prePaymentChan: + s.prePaymentChan = nil + if result.Err != nil { + s.state = StateFailOffchainPayments + s.log.Infof("Failed prepayment: %v", + result.Err) + + return nil, nil + } + s.cost.Server += result.PaidAmt + + // Unexpected error on the confirm channel happened, + // abandon the swap. + case err := <-htlcErrChan: + return nil, err + + // Htlc got confirmed, continue to sweeping. + case htlcConfNtfn := <-htlcConfChan: + txConf = htlcConfNtfn + break loop + + // New block is received. Recheck max reveal height. + case notification := <-s.blockEpochChan: + s.height = notification.(int32) + + logger.Infof("Received block %v", s.height) + + if checkMaxRevealHeightExceeded() { + return nil, nil + } + + // Client quit. + case <-globalCtx.Done(): + return nil, globalCtx.Err() + } + } + + s.log.Infof("Swap script confirmed on chain") + + } else { + s.log.Infof("Retrieving htlc onchain") + select { + case err := <-htlcErrChan: + return nil, err + case htlcConfNtfn := <-htlcConfChan: + txConf = htlcConfNtfn + case <-globalCtx.Done(): + return nil, globalCtx.Err() + } + } + + s.log.Infof("Htlc tx %v at height %v", txConf.Tx.TxHash(), + txConf.BlockHeight) + + return txConf, nil +} + +// waitForHtlcSpendConfirmed waits for the htlc to be spent either by our own +// sweep or a server revocation tx. During this process, this function will try +// to spend the htlc every block by calling spendFunc. +// +// TODO: Improve retry/fee increase mechanism. Once in the mempool, server can +// sweep offchain. So we must make sure we sweep successfully before on-chain +// timeout. +func (s *unchargeSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, + spendFunc func() error) (*chainntnfs.SpendDetail, error) { + + // Register the htlc spend notification. + ctx, cancel := context.WithCancel(globalCtx) + defer cancel() + spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( + ctx, nil, s.htlc.ScriptHash, s.InitiationHeight, + ) + if err != nil { + return nil, fmt.Errorf("register spend ntfn: %v", err) + } + + timerChan := s.timerFactory(republishDelay) + for { + select { + // Htlc spend, break loop. + case spendDetails := <-spendChan: + s.log.Infof("Htlc spend by tx: %v", spendDetails.SpenderTxHash) + + return spendDetails, nil + + // Spend notification error. + case err := <-spendErr: + return nil, err + + // New block arrived, update height and restart the republish + // timer. + case notification := <-s.blockEpochChan: + s.height = notification.(int32) + timerChan = s.timerFactory(republishDelay) + + // Some time after start or after arrival of a new block, try + // to spend again. + case <-timerChan: + err := spendFunc() + if err != nil { + return nil, err + } + + // Context canceled. + case <-globalCtx.Done(): + return nil, globalCtx.Err() + } + } +} + +// sweep tries to sweep the given htlc to a destination address. It takes into +// account the max miner fee and marks the preimage as revealed when it +// published the tx. +// +// TODO: Use lnd sweeper? +func (s *unchargeSwap) sweep(ctx context.Context, + htlcOutpoint wire.OutPoint, + htlcValue btcutil.Amount) error { + + witnessFunc := func(sig []byte) (wire.TxWitness, error) { + return s.htlc.GenSuccessWitness( + sig, s.Preimage, + ) + } + + // Calculate sweep tx fee + fee, err := s.sweeper.GetSweepFee( + ctx, s.htlc.MaxSuccessWitnessSize, + s.SweepConfTarget, + ) + if err != nil { + return err + } + + if fee > s.MaxMinerFee { + s.log.Warnf("Required miner fee %v exceeds max of %v", + fee, s.MaxMinerFee) + + if s.state == StatePreimageRevealed { + // The currently required fee exceeds the max, but we + // already revealed the preimage. The best we can do now + // is to republish with the max fee. + fee = s.MaxMinerFee + } else { + s.log.Warnf("Not revealing preimage") + return nil + } + } + + // Create sweep tx. + sweepTx, err := s.sweeper.CreateSweepTx( + ctx, s.height, s.htlc, htlcOutpoint, + s.ReceiverKey, witnessFunc, + htlcValue, fee, s.DestAddr, + ) + if err != nil { + return err + } + + // Before publishing the tx, already mark the preimage as revealed. This + // is a precaution in case the publish call never returns and would + // leave us thinking we didn't reveal yet. + if s.state != StatePreimageRevealed { + s.state = StatePreimageRevealed + + err := s.persistState(ctx) + if err != nil { + return err + } + } + + // Publish tx. + s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)", + s.DestAddr, fee, sweepTx.TxHash()) + + err = s.lnd.WalletKit.PublishTransaction(ctx, sweepTx) + if err != nil { + s.log.Warnf("Publish sweep: %v", err) + } + + return nil +} + +// validateUnchargeContract validates the contract parameters against our +// request. +func validateUnchargeContract(lnd *lndclient.LndServices, + height int32, + request *UnchargeRequest, + response *newUnchargeResponse) error { + + // Check invoice amounts. + chainParams := lnd.ChainParams + + swapInvoiceAmt, err := utils.GetInvoiceAmt( + chainParams, response.swapInvoice, + ) + if err != nil { + return err + } + + prepayInvoiceAmt, err := utils.GetInvoiceAmt( + chainParams, response.prepayInvoice, + ) + if err != nil { + return err + } + + swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount + if swapFee > request.MaxSwapFee { + logger.Warnf("Swap fee %v exceeding maximum of %v", + swapFee, request.MaxSwapFee) + + return ErrSwapFeeTooHigh + } + + if prepayInvoiceAmt > request.MaxPrepayAmount { + logger.Warnf("Prepay amount %v exceeding maximum of %v", + prepayInvoiceAmt, request.MaxPrepayAmount) + + return ErrPrepayAmountTooHigh + } + + if response.expiry-height < MinUnchargePreimageRevealDelta { + logger.Warnf("Proposed expiry %v (delta %v) too soon", + response.expiry, response.expiry-height) + + return ErrExpiryTooSoon + } + + return nil +} diff --git a/client/uncharge_state.go b/client/uncharge_state.go new file mode 100644 index 0000000..23a776a --- /dev/null +++ b/client/uncharge_state.go @@ -0,0 +1,89 @@ +package client + +// SwapState indicates the current state of a swap. +type SwapState uint8 + +const ( + // StateInitiated is the initial state of a swap. At that point, the + // initiation call to the server has been made and the payment process + // has been started for the swap and prepayment invoices. + StateInitiated SwapState = iota + + // StatePreimageRevealed is reached when the sweep tx publication is + // first attempted. From that point on, we should consider the preimage + // to no longer be secret and we need to do all we can to get the sweep + // confirmed. This state will mostly coalesce with StateHtlcConfirmed, + // except in the case where we wait for fees to come down before we + // sweep. + StatePreimageRevealed + + // StateSuccess is the final swap state that is reached when the sweep + // tx has the required confirmation depth (SweepConfDepth) and the + // server pulled the off-chain htlc. + StateSuccess + + // StateFailOffchainPayments indicates that it wasn't possible to find routes + // for one or both of the off-chain payments to the server that + // satisfied the payment restrictions (fee and timelock limits). + StateFailOffchainPayments + + // StateFailTimeout indicates that the on-chain htlc wasn't confirmed before + // its expiry or confirmed too late (MinPreimageRevealDelta violated). + StateFailTimeout + + // StateFailSweepTimeout indicates that the on-chain htlc wasn't swept before + // the server revoked the htlc. The server didn't pull the off-chain + // htlc (even though it could have) and we timed out the off-chain htlc + // ourselves. No funds lost. + StateFailSweepTimeout + + // StateFailInsufficientValue indicates that the published on-chain htlc had + // a value lower than the requested amount. + StateFailInsufficientValue + + // StateFailTemporary indicates that the swap cannot progress because + // of an internal error. This is not a final state. Manual intervention + // (like a restart) is required to solve this problem. + StateFailTemporary + + // StateHtlcPublished means that the client published the on-chain htlc. + StateHtlcPublished +) + +// Type returns the type of the SwapState it is called on. +func (s SwapState) Type() SwapStateType { + if s == StateInitiated || s == StateHtlcPublished || + s == StatePreimageRevealed || s == StateFailTemporary { + + return StateTypePending + } + + if s == StateSuccess { + return StateTypeSuccess + } + + return StateTypeFail +} + +func (s SwapState) String() string { + switch s { + case StateInitiated: + return "Initiated" + case StatePreimageRevealed: + return "PreimageRevealed" + case StateSuccess: + return "Success" + case StateFailOffchainPayments: + return "FailOffchainPayments" + case StateFailTimeout: + return "FailTimeout" + case StateFailSweepTimeout: + return "FailSweepTimeout" + case StateFailInsufficientValue: + return "FailInsufficientValue" + case StateFailTemporary: + return "FailTemporary" + default: + return "Unknown" + } +} diff --git a/client/uncharge_test.go b/client/uncharge_test.go new file mode 100644 index 0000000..5fdab28 --- /dev/null +++ b/client/uncharge_test.go @@ -0,0 +1,100 @@ +package client + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/sweep" + "github.com/lightninglabs/nautilus/test" +) + +// TestLateHtlcPublish tests that the client is not revealing the preimage if +// there are not enough blocks left. +func TestLateHtlcPublish(t *testing.T) { + defer test.Guard(t)() + + lnd := test.NewMockLnd() + + ctx := test.NewContext(t, lnd) + + server := newServerMock() + + store := newStoreMock(t) + + expiryChan := make(chan time.Time) + timerFactory := func(expiry time.Duration) <-chan time.Time { + return expiryChan + } + + height := int32(600) + + cfg := &swapConfig{ + lnd: &lnd.LndServices, + store: store, + server: server, + } + + swap, err := newUnchargeSwap( + context.Background(), cfg, height, testRequest, + ) + if err != nil { + t.Fatal(err) + } + + sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices} + + blockEpochChan := make(chan interface{}) + statusChan := make(chan SwapInfo) + + errChan := make(chan error) + go func() { + err := swap.execute(context.Background(), &executeConfig{ + statusChan: statusChan, + sweeper: sweeper, + blockEpochChan: blockEpochChan, + timerFactory: timerFactory, + }, height) + if err != nil { + logger.Error(err) + } + errChan <- err + }() + + store.assertUnchargeStored() + + state := <-statusChan + if state.State != StateInitiated { + t.Fatal("unexpected state") + } + + signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) + signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) + + // Expect client to register for conf + ctx.AssertRegisterConf() + + // // Wait too long before publishing htlc. + blockEpochChan <- int32(swap.CltvExpiry - 10) + + signalSwapPaymentResult( + errors.New(lndclient.PaymentResultUnknownPaymentHash), + ) + signalPrepaymentResult( + errors.New(lndclient.PaymentResultUnknownPaymentHash), + ) + + store.assertStoreFinished(StateFailTimeout) + + status := <-statusChan + if status.State != StateFailTimeout { + t.Fatal("unexpected state") + } + + err = <-errChan + if err != nil { + t.Fatal(err) + } +} diff --git a/client/utils.go b/client/utils.go new file mode 100644 index 0000000..8937bc8 --- /dev/null +++ b/client/utils.go @@ -0,0 +1,20 @@ +package client + +import ( + "errors" + + "github.com/btcsuite/btcd/wire" +) + +// getTxInputByOutpoint returns a tx input based on a given input outpoint. +func getTxInputByOutpoint(tx *wire.MsgTx, input *wire.OutPoint) ( + *wire.TxIn, error) { + + for _, in := range tx.TxIn { + if in.PreviousOutPoint == *input { + return in, nil + } + } + + return nil, errors.New("input not found") +} diff --git a/cmd/swapcli/main.go b/cmd/swapcli/main.go new file mode 100644 index 0000000..4a91106 --- /dev/null +++ b/cmd/swapcli/main.go @@ -0,0 +1,300 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "strconv" + "time" + + "github.com/lightninglabs/nautilus/utils" + + "github.com/btcsuite/btcutil" + + "github.com/lightninglabs/nautilus/cmd/swapd/rpc" + "github.com/urfave/cli" + "google.golang.org/grpc" +) + +var ( + swapdAddress = "localhost:11010" + + // Define route independent max routing fees. We have currently no way + // to get a reliable estimate of the routing fees. Best we can do is the + // minimum routing fees, which is not very indicative. + maxRoutingFeeBase = btcutil.Amount(10) + maxRoutingFeeRate = int64(50000) +) + +var unchargeCommand = cli.Command{ + Name: "uncharge", + Usage: "perform an off-chain to on-chain swap", + ArgsUsage: "amt [addr]", + Description: ` + Send the amount in satoshis specified by the amt argument on-chain. + + Optionally a BASE58 encoded bitcoin destination address may be + specified. If not specified, a new wallet address will be generated.`, + Flags: []cli.Flag{ + cli.Uint64Flag{ + Name: "channel", + Usage: "the 8-byte compact channel ID of the channel to uncharge", + }, + }, + Action: uncharge, +} + +var termsCommand = cli.Command{ + Name: "terms", + Usage: "show current server swap terms", + Action: terms, +} + +func main() { + app := cli.NewApp() + + app.Version = "0.0.1" + app.Usage = "command line interface to swapd" + app.Commands = []cli.Command{unchargeCommand, termsCommand} + app.Action = monitor + + err := app.Run(os.Args) + if err != nil { + fmt.Println(err) + } +} + +func terms(ctx *cli.Context) error { + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + terms, err := client.GetUnchargeTerms( + context.Background(), &rpc.TermsRequest{}, + ) + if err != nil { + return err + } + + fmt.Printf("Amount: %d - %d\n", + btcutil.Amount(terms.MinSwapAmount), + btcutil.Amount(terms.MaxSwapAmount), + ) + if err != nil { + return err + } + + printTerms := func(terms *rpc.TermsResponse) { + fmt.Printf("Amount: %d - %d\n", + btcutil.Amount(terms.MinSwapAmount), + btcutil.Amount(terms.MaxSwapAmount), + ) + fmt.Printf("Fee: %d + %.4f %% (%d prepaid)\n", + btcutil.Amount(terms.SwapFeeBase), + utils.FeeRateAsPercentage(terms.SwapFeeRate), + btcutil.Amount(terms.PrepayAmt), + ) + + fmt.Printf("Cltv delta: %v blocks\n", terms.CltvDelta) + } + + fmt.Println("Uncharge") + fmt.Println("--------") + printTerms(terms) + + return nil +} + +func monitor(ctx *cli.Context) error { + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + stream, err := client.Monitor( + context.Background(), &rpc.MonitorRequest{}) + if err != nil { + return err + } + + for { + swap, err := stream.Recv() + if err != nil { + return fmt.Errorf("recv: %v", err) + } + logSwap(swap) + } +} + +func getClient(ctx *cli.Context) (rpc.SwapClientClient, func(), error) { + conn, err := getSwapCliConn(swapdAddress) + if err != nil { + return nil, nil, err + } + cleanup := func() { conn.Close() } + + swapCliClient := rpc.NewSwapClientClient(conn) + return swapCliClient, cleanup, nil +} + +func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { + return utils.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate) +} + +type limits struct { + maxSwapRoutingFee btcutil.Amount + maxPrepayRoutingFee btcutil.Amount + maxMinerFee btcutil.Amount + maxSwapFee btcutil.Amount + maxPrepayAmt btcutil.Amount +} + +func getLimits(amt btcutil.Amount, quote *rpc.QuoteResponse) *limits { + return &limits{ + maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)), + maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount( + quote.PrepayAmt, + )), + + // Apply a multiplier to the estimated miner fee, to not get the swap + // canceled because fees increased in the mean time. + maxMinerFee: btcutil.Amount(quote.MinerFee) * 3, + + maxSwapFee: btcutil.Amount(quote.SwapFee), + maxPrepayAmt: btcutil.Amount(quote.PrepayAmt), + } +} + +func displayLimits(amt btcutil.Amount, l *limits) error { + totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee + + l.maxMinerFee + l.maxSwapFee + + fmt.Printf("Max swap fees for %d uncharge: %d\n", + btcutil.Amount(amt), totalSuccessMax, + ) + fmt.Printf("CONTINUE SWAP? (y/n), expand fee detail (x): ") + var answer string + fmt.Scanln(&answer) + switch answer { + case "y": + return nil + case "x": + fmt.Println() + fmt.Printf("Max on-chain fee: %d\n", l.maxMinerFee) + fmt.Printf("Max off-chain swap routing fee: %d\n", + l.maxSwapRoutingFee) + fmt.Printf("Max off-chain prepay routing fee: %d\n", + l.maxPrepayRoutingFee) + fmt.Printf("Max swap fee: %d\n", l.maxSwapFee) + fmt.Printf("Max no show penalty: %d\n", + l.maxPrepayAmt) + + fmt.Printf("CONTINUE SWAP? (y/n): ") + fmt.Scanln(&answer) + if answer == "y" { + return nil + } + } + + return errors.New("swap canceled") +} + +func parseAmt(text string) (btcutil.Amount, error) { + amtInt64, err := strconv.ParseInt(text, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid amt value") + } + return btcutil.Amount(amtInt64), nil +} + +func uncharge(ctx *cli.Context) error { + // Show command help if no arguments and flags were provided. + if ctx.NArg() < 1 { + cli.ShowCommandHelp(ctx, "uncharge") + return nil + } + + args := ctx.Args() + + amt, err := parseAmt(args[0]) + if err != nil { + return err + } + + var destAddr string + args = args.Tail() + if args.Present() { + destAddr = args.First() + } + + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + quote, err := client.GetUnchargeQuote( + context.Background(), + &rpc.QuoteRequest{ + Amt: int64(amt), + }, + ) + if err != nil { + return err + } + + limits := getLimits(amt, quote) + + if err := displayLimits(amt, limits); err != nil { + return err + } + + var unchargeChannel uint64 + if ctx.IsSet("channel") { + unchargeChannel = ctx.Uint64("channel") + } + + resp, err := client.Uncharge(context.Background(), &rpc.UnchargeRequest{ + Amt: int64(amt), + Dest: destAddr, + MaxMinerFee: int64(limits.maxMinerFee), + MaxPrepayAmt: int64(limits.maxPrepayAmt), + MaxSwapFee: int64(limits.maxSwapFee), + MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee), + MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee), + UnchargeChannel: unchargeChannel, + }) + if err != nil { + return err + } + + fmt.Printf("Swap initiated with id: %v\n", resp.Id[:8]) + fmt.Printf("Run swapcli without a command to monitor progress.\n") + + return nil +} + +func logSwap(swap *rpc.SwapStatus) { + fmt.Printf("%v %v %v %v - %v\n", + time.Unix(0, swap.LastUpdateTime).Format(time.RFC3339), + swap.Type, swap.State, btcutil.Amount(swap.Amt), + swap.HtlcAddress, + ) +} + +func getSwapCliConn(address string) (*grpc.ClientConn, error) { + opts := []grpc.DialOption{ + grpc.WithInsecure(), + } + + conn, err := grpc.Dial(address, opts...) + if err != nil { + return nil, fmt.Errorf("unable to connect to RPC server: %v", err) + } + + return conn, nil +} diff --git a/cmd/swapd/daemon.go b/cmd/swapd/daemon.go new file mode 100644 index 0000000..e43c352 --- /dev/null +++ b/cmd/swapd/daemon.go @@ -0,0 +1,156 @@ +package main + +import ( + "context" + "fmt" + "net" + "os" + "os/signal" + "runtime/pprof" + "sync" + "time" + + "github.com/lightninglabs/nautilus/client" + clientrpc "github.com/lightninglabs/nautilus/cmd/swapd/rpc" + "github.com/urfave/cli" + "google.golang.org/grpc" +) + +// daemon runs swapd in daemon mode. It will listen for grpc connections, +// execute commands and pass back swap status information. +func daemon(ctx *cli.Context) error { + lnd, err := getLnd(ctx) + if err != nil { + return err + } + defer lnd.Close() + + swapClient, cleanup, err := getClient(ctx, &lnd.LndServices) + if err != nil { + return err + } + defer cleanup() + + // Before starting the client, build an in-memory view of all swaps. + // This view is used to update newly connected clients with the most + // recent swaps. + storedSwaps, err := swapClient.GetUnchargeSwaps() + if err != nil { + return err + } + for _, swap := range storedSwaps { + swaps[swap.Hash] = client.SwapInfo{ + SwapType: client.SwapTypeUncharge, + SwapContract: swap.Contract.SwapContract, + State: swap.State(), + SwapHash: swap.Hash, + LastUpdate: swap.LastUpdateTime(), + } + } + + // Instantiate the swapd gRPC server. + server := swapClientServer{ + impl: swapClient, + lnd: &lnd.LndServices, + } + + serverOpts := []grpc.ServerOption{} + grpcServer := grpc.NewServer(serverOpts...) + clientrpc.RegisterSwapClientServer(grpcServer, &server) + + // Next, Start the gRPC server listening for HTTP/2 connections. + logger.Infof("Starting RPC listener") + lis, err := net.Listen("tcp", defaultListenAddr) + if err != nil { + return fmt.Errorf("RPC server unable to listen on %s", + defaultListenAddr) + + } + defer lis.Close() + + statusChan := make(chan client.SwapInfo) + + mainCtx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + + // Start the swap client itself. + wg.Add(1) + go func() { + defer wg.Done() + + logger.Infof("Starting swap client") + err := swapClient.Run(mainCtx, statusChan) + if err != nil { + logger.Error(err) + } + logger.Infof("Swap client stopped") + + logger.Infof("Stopping gRPC server") + grpcServer.Stop() + + cancel() + }() + + // Start a goroutine that broadcasts swap updates to clients. + wg.Add(1) + go func() { + defer wg.Done() + + logger.Infof("Waiting for updates") + for { + select { + case swap := <-statusChan: + swapsLock.Lock() + swaps[swap.SwapHash] = swap + + for _, subscriber := range subscribers { + select { + case subscriber <- swap: + case <-mainCtx.Done(): + return + } + } + + swapsLock.Unlock() + case <-mainCtx.Done(): + return + } + } + }() + + // Start the grpc server. + wg.Add(1) + go func() { + defer wg.Done() + + logger.Infof("RPC server listening on %s", lis.Addr()) + + err = grpcServer.Serve(lis) + if err != nil { + logger.Error(err) + } + }() + + interruptChannel := make(chan os.Signal, 1) + signal.Notify(interruptChannel, os.Interrupt) + + // Run until the users terminates swapd or an error occurred. + select { + case <-interruptChannel: + logger.Infof("Received SIGINT (Ctrl+C).") + + // TODO: Remove debug code. + // Debug code to dump goroutines on hanging exit. + go func() { + time.Sleep(5 * time.Second) + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + }() + + cancel() + case <-mainCtx.Done(): + } + + wg.Wait() + + return nil +} diff --git a/cmd/swapd/log.go b/cmd/swapd/log.go new file mode 100644 index 0000000..d31f344 --- /dev/null +++ b/cmd/swapd/log.go @@ -0,0 +1,24 @@ +package main + +import ( + "os" + + "github.com/btcsuite/btclog" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var ( + backendLog = btclog.NewBackend(logWriter{}) + logger = backendLog.Logger("SWAPD") +) + +// logWriter implements an io.Writer that outputs to both standard output and +// the write-end pipe of an initialized log rotator. +type logWriter struct{} + +func (logWriter) Write(p []byte) (n int, err error) { + os.Stdout.Write(p) + return len(p), nil +} diff --git a/cmd/swapd/main.go b/cmd/swapd/main.go new file mode 100644 index 0000000..a45b4f1 --- /dev/null +++ b/cmd/swapd/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "os" + "sync" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/client" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/urfave/cli" +) + +const ( + defaultListenPort = 11010 + defaultConfTarget = int32(2) +) + +var ( + defaultListenAddr = fmt.Sprintf("localhost:%d", defaultListenPort) + defaultSwapletDir = btcutil.AppDataDir("swaplet", false) + + swaps = make(map[lntypes.Hash]client.SwapInfo) + subscribers = make(map[int]chan<- interface{}) + nextSubscriberID int + swapsLock sync.Mutex +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "network", + Value: "mainnet", + Usage: "network to run on (regtest, testnet, mainnet)", + }, + cli.StringFlag{ + Name: "lnd", + Value: "localhost:10009", + Usage: "lnd instance rpc address host:port", + }, + cli.StringFlag{ + Name: "swapserver", + Value: "swap.lightning.today:11009", + Usage: "swap server address host:port", + }, + cli.StringFlag{ + Name: "macaroonpath", + Usage: "path to lnd macaroon", + }, + cli.StringFlag{ + Name: "tlspath", + Usage: "path to lnd tls certificate", + }, + cli.BoolFlag{ + Name: "insecure", + Usage: "disable tls", + }, + } + app.Version = "0.0.1" + app.Usage = "swaps execution daemon" + app.Commands = []cli.Command{viewCommand} + app.Action = daemon + + err := app.Run(os.Args) + if err != nil { + fmt.Println(err) + } +} diff --git a/cmd/swapd/rpc/gen_protos.sh b/cmd/swapd/rpc/gen_protos.sh new file mode 100755 index 0000000..b6be937 --- /dev/null +++ b/cmd/swapd/rpc/gen_protos.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Generate the protos. +protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + --go_out=plugins=grpc:. \ + swapclient.proto diff --git a/cmd/swapd/rpc/swapclient.pb.go b/cmd/swapd/rpc/swapclient.pb.go new file mode 100644 index 0000000..1161199 --- /dev/null +++ b/cmd/swapd/rpc/swapclient.pb.go @@ -0,0 +1,925 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: swapclient.proto + +package rpc + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type SwapType int32 + +const ( + // UNCHARGE indicates an uncharge swap (off-chain to on-chain) + SwapType_UNCHARGE SwapType = 0 +) + +var SwapType_name = map[int32]string{ + 0: "UNCHARGE", +} +var SwapType_value = map[string]int32{ + "UNCHARGE": 0, +} + +func (x SwapType) String() string { + return proto.EnumName(SwapType_name, int32(x)) +} +func (SwapType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{0} +} + +type SwapState int32 + +const ( + // * + // INITIATED is the initial state of a swap. At that point, the initiation + // call to the server has been made and the payment process has been started + // for the swap and prepayment invoices. + SwapState_INITIATED SwapState = 0 + // * + // PREIMAGE_REVEALED is reached when the sweep tx publication is first + // attempted. From that point on, we should consider the preimage to no + // longer be secret and we need to do all we can to get the sweep confirmed. + // This state will mostly coalesce with StateHtlcConfirmed, except in the + // case where we wait for fees to come down before we sweep. + SwapState_PREIMAGE_REVEALED SwapState = 1 + // * + // SUCCESS is the final swap state that is reached when the sweep tx has + // the required confirmation depth. + SwapState_SUCCESS SwapState = 3 + // * + // FAILED is the final swap state for a failed swap with or without loss of + // the swap amount. + SwapState_FAILED SwapState = 4 +) + +var SwapState_name = map[int32]string{ + 0: "INITIATED", + 1: "PREIMAGE_REVEALED", + 3: "SUCCESS", + 4: "FAILED", +} +var SwapState_value = map[string]int32{ + "INITIATED": 0, + "PREIMAGE_REVEALED": 1, + "SUCCESS": 3, + "FAILED": 4, +} + +func (x SwapState) String() string { + return proto.EnumName(SwapState_name, int32(x)) +} +func (SwapState) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{1} +} + +type UnchargeRequest struct { + // * + // Requested swap amount in sat. This does not include the swap and miner + // fee. + Amt int64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"` + // * + // Base58 encoded destination address for the swap. + Dest string `protobuf:"bytes,2,opt,name=dest,proto3" json:"dest,omitempty"` + // * + // Maximum off-chain fee in msat that may be paid for payment to the server. + // This limit is applied during path finding. Typically this value is taken + // from the response of the GetQuote call. + MaxSwapRoutingFee int64 `protobuf:"varint,3,opt,name=max_swap_routing_fee,json=maxSwapRoutingFee,proto3" json:"max_swap_routing_fee,omitempty"` + // * + // Maximum off-chain fee in msat that may be paid for payment to the server. + // This limit is applied during path finding. Typically this value is taken + // from the response of the GetQuote call. + MaxPrepayRoutingFee int64 `protobuf:"varint,4,opt,name=max_prepay_routing_fee,json=maxPrepayRoutingFee,proto3" json:"max_prepay_routing_fee,omitempty"` + // * + // Maximum we are willing to pay the server for the swap. This value is not + // disclosed in the swap initiation call, but if the server asks for a + // higher fee, we abort the swap. Typically this value is taken from the + // response of the GetQuote call. It includes the prepay amount. + MaxSwapFee int64 `protobuf:"varint,5,opt,name=max_swap_fee,json=maxSwapFee,proto3" json:"max_swap_fee,omitempty"` + // * + // Maximum amount of the swap fee that may be charged as a prepayment. + MaxPrepayAmt int64 `protobuf:"varint,6,opt,name=max_prepay_amt,json=maxPrepayAmt,proto3" json:"max_prepay_amt,omitempty"` + // * + // Maximum in on-chain fees that we are willing to spent. If we want to + // sweep the on-chain htlc and the fee estimate turns out higher than this + // value, we cancel the swap. If the fee estimate is lower, we publish the + // sweep tx. + // + // If the sweep tx isn't confirmed, we are forced to ratchet up fees until + // it is swept. Possibly even exceeding max_miner_fee if we get close to the + // htlc timeout. Because the initial publication revealed the preimage, we + // have no other choice. The server may already have pulled the off-chain + // htlc. Only when the fee becomes higher than the swap amount, we can only + // wait for fees to come down and hope - if we are past the timeout - that + // the server isn't publishing the revocation. + // + // max_miner_fee is typically taken from the response of the GetQuote call. + MaxMinerFee int64 `protobuf:"varint,7,opt,name=max_miner_fee,json=maxMinerFee,proto3" json:"max_miner_fee,omitempty"` + // * + // The channel to uncharge. If zero, the channel to uncharge is selected based + // on the lowest routing fee for the swap payment to the server. + UnchargeChannel uint64 `protobuf:"varint,8,opt,name=uncharge_channel,json=unchargeChannel,proto3" json:"uncharge_channel,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *UnchargeRequest) Reset() { *m = UnchargeRequest{} } +func (m *UnchargeRequest) String() string { return proto.CompactTextString(m) } +func (*UnchargeRequest) ProtoMessage() {} +func (*UnchargeRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{0} +} +func (m *UnchargeRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_UnchargeRequest.Unmarshal(m, b) +} +func (m *UnchargeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_UnchargeRequest.Marshal(b, m, deterministic) +} +func (dst *UnchargeRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_UnchargeRequest.Merge(dst, src) +} +func (m *UnchargeRequest) XXX_Size() int { + return xxx_messageInfo_UnchargeRequest.Size(m) +} +func (m *UnchargeRequest) XXX_DiscardUnknown() { + xxx_messageInfo_UnchargeRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_UnchargeRequest proto.InternalMessageInfo + +func (m *UnchargeRequest) GetAmt() int64 { + if m != nil { + return m.Amt + } + return 0 +} + +func (m *UnchargeRequest) GetDest() string { + if m != nil { + return m.Dest + } + return "" +} + +func (m *UnchargeRequest) GetMaxSwapRoutingFee() int64 { + if m != nil { + return m.MaxSwapRoutingFee + } + return 0 +} + +func (m *UnchargeRequest) GetMaxPrepayRoutingFee() int64 { + if m != nil { + return m.MaxPrepayRoutingFee + } + return 0 +} + +func (m *UnchargeRequest) GetMaxSwapFee() int64 { + if m != nil { + return m.MaxSwapFee + } + return 0 +} + +func (m *UnchargeRequest) GetMaxPrepayAmt() int64 { + if m != nil { + return m.MaxPrepayAmt + } + return 0 +} + +func (m *UnchargeRequest) GetMaxMinerFee() int64 { + if m != nil { + return m.MaxMinerFee + } + return 0 +} + +func (m *UnchargeRequest) GetUnchargeChannel() uint64 { + if m != nil { + return m.UnchargeChannel + } + return 0 +} + +type SwapResponse struct { + // * + // Swap identifier to track status in the update stream that is returned from + // the Start() call. Currently this is the hash that locks the htlcs. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SwapResponse) Reset() { *m = SwapResponse{} } +func (m *SwapResponse) String() string { return proto.CompactTextString(m) } +func (*SwapResponse) ProtoMessage() {} +func (*SwapResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{1} +} +func (m *SwapResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SwapResponse.Unmarshal(m, b) +} +func (m *SwapResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SwapResponse.Marshal(b, m, deterministic) +} +func (dst *SwapResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_SwapResponse.Merge(dst, src) +} +func (m *SwapResponse) XXX_Size() int { + return xxx_messageInfo_SwapResponse.Size(m) +} +func (m *SwapResponse) XXX_DiscardUnknown() { + xxx_messageInfo_SwapResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_SwapResponse proto.InternalMessageInfo + +func (m *SwapResponse) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +type MonitorRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MonitorRequest) Reset() { *m = MonitorRequest{} } +func (m *MonitorRequest) String() string { return proto.CompactTextString(m) } +func (*MonitorRequest) ProtoMessage() {} +func (*MonitorRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{2} +} +func (m *MonitorRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_MonitorRequest.Unmarshal(m, b) +} +func (m *MonitorRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_MonitorRequest.Marshal(b, m, deterministic) +} +func (dst *MonitorRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_MonitorRequest.Merge(dst, src) +} +func (m *MonitorRequest) XXX_Size() int { + return xxx_messageInfo_MonitorRequest.Size(m) +} +func (m *MonitorRequest) XXX_DiscardUnknown() { + xxx_messageInfo_MonitorRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_MonitorRequest proto.InternalMessageInfo + +type SwapStatus struct { + // * + // Requested swap amount in sat. This does not include the swap and miner + // fee. + Amt int64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"` + // * + // Swap identifier to track status in the update stream that is returned from + // the Start() call. Currently this is the hash that locks the htlcs. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // * + // Swap type + Type SwapType `protobuf:"varint,3,opt,name=type,proto3,enum=rpc.SwapType" json:"type,omitempty"` + // * + // State the swap is currently in, see State enum. + State SwapState `protobuf:"varint,4,opt,name=state,proto3,enum=rpc.SwapState" json:"state,omitempty"` + // * + // Initiation time of the swap. + InitiationTime int64 `protobuf:"varint,5,opt,name=initiation_time,json=initiationTime,proto3" json:"initiation_time,omitempty"` + // * + // Initiation time of the swap. + LastUpdateTime int64 `protobuf:"varint,6,opt,name=last_update_time,json=lastUpdateTime,proto3" json:"last_update_time,omitempty"` + // * + // Htlc address. + HtlcAddress string `protobuf:"bytes,7,opt,name=htlc_address,json=htlcAddress,proto3" json:"htlc_address,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SwapStatus) Reset() { *m = SwapStatus{} } +func (m *SwapStatus) String() string { return proto.CompactTextString(m) } +func (*SwapStatus) ProtoMessage() {} +func (*SwapStatus) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{3} +} +func (m *SwapStatus) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SwapStatus.Unmarshal(m, b) +} +func (m *SwapStatus) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SwapStatus.Marshal(b, m, deterministic) +} +func (dst *SwapStatus) XXX_Merge(src proto.Message) { + xxx_messageInfo_SwapStatus.Merge(dst, src) +} +func (m *SwapStatus) XXX_Size() int { + return xxx_messageInfo_SwapStatus.Size(m) +} +func (m *SwapStatus) XXX_DiscardUnknown() { + xxx_messageInfo_SwapStatus.DiscardUnknown(m) +} + +var xxx_messageInfo_SwapStatus proto.InternalMessageInfo + +func (m *SwapStatus) GetAmt() int64 { + if m != nil { + return m.Amt + } + return 0 +} + +func (m *SwapStatus) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *SwapStatus) GetType() SwapType { + if m != nil { + return m.Type + } + return SwapType_UNCHARGE +} + +func (m *SwapStatus) GetState() SwapState { + if m != nil { + return m.State + } + return SwapState_INITIATED +} + +func (m *SwapStatus) GetInitiationTime() int64 { + if m != nil { + return m.InitiationTime + } + return 0 +} + +func (m *SwapStatus) GetLastUpdateTime() int64 { + if m != nil { + return m.LastUpdateTime + } + return 0 +} + +func (m *SwapStatus) GetHtlcAddress() string { + if m != nil { + return m.HtlcAddress + } + return "" +} + +type TermsRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TermsRequest) Reset() { *m = TermsRequest{} } +func (m *TermsRequest) String() string { return proto.CompactTextString(m) } +func (*TermsRequest) ProtoMessage() {} +func (*TermsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{4} +} +func (m *TermsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TermsRequest.Unmarshal(m, b) +} +func (m *TermsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TermsRequest.Marshal(b, m, deterministic) +} +func (dst *TermsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_TermsRequest.Merge(dst, src) +} +func (m *TermsRequest) XXX_Size() int { + return xxx_messageInfo_TermsRequest.Size(m) +} +func (m *TermsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_TermsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_TermsRequest proto.InternalMessageInfo + +type TermsResponse struct { + // * + // The node pubkey where the swap payment needs to be paid + // to. This can be used to test connectivity before initiating the swap. + SwapPaymentDest string `protobuf:"bytes,1,opt,name=swap_payment_dest,json=swapPaymentDest,proto3" json:"swap_payment_dest,omitempty"` + // * + // The base fee for a swap (sat) + SwapFeeBase int64 `protobuf:"varint,2,opt,name=swap_fee_base,json=swapFeeBase,proto3" json:"swap_fee_base,omitempty"` + // * + // The fee rate for a swap (parts per million) + SwapFeeRate int64 `protobuf:"varint,3,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"` + // * + // Required prepay amount + PrepayAmt int64 `protobuf:"varint,4,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"` + // * + // Minimum swap amount (sat) + MinSwapAmount int64 `protobuf:"varint,5,opt,name=min_swap_amount,json=minSwapAmount,proto3" json:"min_swap_amount,omitempty"` + // * + // Maximum swap amount (sat) + MaxSwapAmount int64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"` + // * + // On-chain cltv expiry delta + CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"` + // * + // Maximum cltv expiry delta + MaxCltv int32 `protobuf:"varint,8,opt,name=max_cltv,json=maxCltv,proto3" json:"max_cltv,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TermsResponse) Reset() { *m = TermsResponse{} } +func (m *TermsResponse) String() string { return proto.CompactTextString(m) } +func (*TermsResponse) ProtoMessage() {} +func (*TermsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{5} +} +func (m *TermsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TermsResponse.Unmarshal(m, b) +} +func (m *TermsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TermsResponse.Marshal(b, m, deterministic) +} +func (dst *TermsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_TermsResponse.Merge(dst, src) +} +func (m *TermsResponse) XXX_Size() int { + return xxx_messageInfo_TermsResponse.Size(m) +} +func (m *TermsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_TermsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_TermsResponse proto.InternalMessageInfo + +func (m *TermsResponse) GetSwapPaymentDest() string { + if m != nil { + return m.SwapPaymentDest + } + return "" +} + +func (m *TermsResponse) GetSwapFeeBase() int64 { + if m != nil { + return m.SwapFeeBase + } + return 0 +} + +func (m *TermsResponse) GetSwapFeeRate() int64 { + if m != nil { + return m.SwapFeeRate + } + return 0 +} + +func (m *TermsResponse) GetPrepayAmt() int64 { + if m != nil { + return m.PrepayAmt + } + return 0 +} + +func (m *TermsResponse) GetMinSwapAmount() int64 { + if m != nil { + return m.MinSwapAmount + } + return 0 +} + +func (m *TermsResponse) GetMaxSwapAmount() int64 { + if m != nil { + return m.MaxSwapAmount + } + return 0 +} + +func (m *TermsResponse) GetCltvDelta() int32 { + if m != nil { + return m.CltvDelta + } + return 0 +} + +func (m *TermsResponse) GetMaxCltv() int32 { + if m != nil { + return m.MaxCltv + } + return 0 +} + +type QuoteRequest struct { + // * + // Requested swap amount in sat. + Amt int64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *QuoteRequest) Reset() { *m = QuoteRequest{} } +func (m *QuoteRequest) String() string { return proto.CompactTextString(m) } +func (*QuoteRequest) ProtoMessage() {} +func (*QuoteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{6} +} +func (m *QuoteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_QuoteRequest.Unmarshal(m, b) +} +func (m *QuoteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_QuoteRequest.Marshal(b, m, deterministic) +} +func (dst *QuoteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QuoteRequest.Merge(dst, src) +} +func (m *QuoteRequest) XXX_Size() int { + return xxx_messageInfo_QuoteRequest.Size(m) +} +func (m *QuoteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QuoteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QuoteRequest proto.InternalMessageInfo + +func (m *QuoteRequest) GetAmt() int64 { + if m != nil { + return m.Amt + } + return 0 +} + +type QuoteResponse struct { + // * + // The fee that the swap server is charging for the swap. + SwapFee int64 `protobuf:"varint,1,opt,name=swap_fee,json=swapFee,proto3" json:"swap_fee,omitempty"` + // * + // The part of the swap fee that is requested as a + // prepayment. + PrepayAmt int64 `protobuf:"varint,2,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"` + // * + // An estimate of the on-chain fee that needs to be paid to + // sweep the htlc. + MinerFee int64 `protobuf:"varint,3,opt,name=miner_fee,json=minerFee,proto3" json:"miner_fee,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *QuoteResponse) Reset() { *m = QuoteResponse{} } +func (m *QuoteResponse) String() string { return proto.CompactTextString(m) } +func (*QuoteResponse) ProtoMessage() {} +func (*QuoteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_swapclient_d9c5a6779b6644af, []int{7} +} +func (m *QuoteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_QuoteResponse.Unmarshal(m, b) +} +func (m *QuoteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_QuoteResponse.Marshal(b, m, deterministic) +} +func (dst *QuoteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QuoteResponse.Merge(dst, src) +} +func (m *QuoteResponse) XXX_Size() int { + return xxx_messageInfo_QuoteResponse.Size(m) +} +func (m *QuoteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QuoteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QuoteResponse proto.InternalMessageInfo + +func (m *QuoteResponse) GetSwapFee() int64 { + if m != nil { + return m.SwapFee + } + return 0 +} + +func (m *QuoteResponse) GetPrepayAmt() int64 { + if m != nil { + return m.PrepayAmt + } + return 0 +} + +func (m *QuoteResponse) GetMinerFee() int64 { + if m != nil { + return m.MinerFee + } + return 0 +} + +func init() { + proto.RegisterType((*UnchargeRequest)(nil), "rpc.UnchargeRequest") + proto.RegisterType((*SwapResponse)(nil), "rpc.SwapResponse") + proto.RegisterType((*MonitorRequest)(nil), "rpc.MonitorRequest") + proto.RegisterType((*SwapStatus)(nil), "rpc.SwapStatus") + proto.RegisterType((*TermsRequest)(nil), "rpc.TermsRequest") + proto.RegisterType((*TermsResponse)(nil), "rpc.TermsResponse") + proto.RegisterType((*QuoteRequest)(nil), "rpc.QuoteRequest") + proto.RegisterType((*QuoteResponse)(nil), "rpc.QuoteResponse") + proto.RegisterEnum("rpc.SwapType", SwapType_name, SwapType_value) + proto.RegisterEnum("rpc.SwapState", SwapState_name, SwapState_value) +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// SwapClientClient is the client API for SwapClient service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type SwapClientClient interface { + // * + // Uncharge initiates an uncharge swap with the given parameters. The call + // returns after the swap has been set up with the swap server. From that + // point onwards, progress can be tracked via the SwapStatus stream + // that is returned from Monitor(). + Uncharge(ctx context.Context, in *UnchargeRequest, opts ...grpc.CallOption) (*SwapResponse, error) + // * + // Monitor will return a stream of swap updates for currently active swaps. + Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (SwapClient_MonitorClient, error) + // * + // GetTerms returns the terms that the server enforces for swaps. + GetUnchargeTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) + // * + // GetQuote returns a quote for a swap with the provided parameters. + GetUnchargeQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) +} + +type swapClientClient struct { + cc *grpc.ClientConn +} + +func NewSwapClientClient(cc *grpc.ClientConn) SwapClientClient { + return &swapClientClient{cc} +} + +func (c *swapClientClient) Uncharge(ctx context.Context, in *UnchargeRequest, opts ...grpc.CallOption) (*SwapResponse, error) { + out := new(SwapResponse) + err := c.cc.Invoke(ctx, "/rpc.SwapClient/Uncharge", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapClientClient) Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (SwapClient_MonitorClient, error) { + stream, err := c.cc.NewStream(ctx, &_SwapClient_serviceDesc.Streams[0], "/rpc.SwapClient/Monitor", opts...) + if err != nil { + return nil, err + } + x := &swapClientMonitorClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type SwapClient_MonitorClient interface { + Recv() (*SwapStatus, error) + grpc.ClientStream +} + +type swapClientMonitorClient struct { + grpc.ClientStream +} + +func (x *swapClientMonitorClient) Recv() (*SwapStatus, error) { + m := new(SwapStatus) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *swapClientClient) GetUnchargeTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) { + out := new(TermsResponse) + err := c.cc.Invoke(ctx, "/rpc.SwapClient/GetUnchargeTerms", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapClientClient) GetUnchargeQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) { + out := new(QuoteResponse) + err := c.cc.Invoke(ctx, "/rpc.SwapClient/GetUnchargeQuote", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SwapClientServer is the server API for SwapClient service. +type SwapClientServer interface { + // * + // Uncharge initiates an uncharge swap with the given parameters. The call + // returns after the swap has been set up with the swap server. From that + // point onwards, progress can be tracked via the SwapStatus stream + // that is returned from Monitor(). + Uncharge(context.Context, *UnchargeRequest) (*SwapResponse, error) + // * + // Monitor will return a stream of swap updates for currently active swaps. + Monitor(*MonitorRequest, SwapClient_MonitorServer) error + // * + // GetTerms returns the terms that the server enforces for swaps. + GetUnchargeTerms(context.Context, *TermsRequest) (*TermsResponse, error) + // * + // GetQuote returns a quote for a swap with the provided parameters. + GetUnchargeQuote(context.Context, *QuoteRequest) (*QuoteResponse, error) +} + +func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) { + s.RegisterService(&_SwapClient_serviceDesc, srv) +} + +func _SwapClient_Uncharge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnchargeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).Uncharge(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rpc.SwapClient/Uncharge", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).Uncharge(ctx, req.(*UnchargeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapClient_Monitor_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(MonitorRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SwapClientServer).Monitor(m, &swapClientMonitorServer{stream}) +} + +type SwapClient_MonitorServer interface { + Send(*SwapStatus) error + grpc.ServerStream +} + +type swapClientMonitorServer struct { + grpc.ServerStream +} + +func (x *swapClientMonitorServer) Send(m *SwapStatus) error { + return x.ServerStream.SendMsg(m) +} + +func _SwapClient_GetUnchargeTerms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TermsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).GetUnchargeTerms(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rpc.SwapClient/GetUnchargeTerms", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).GetUnchargeTerms(ctx, req.(*TermsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapClient_GetUnchargeQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QuoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).GetUnchargeQuote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rpc.SwapClient/GetUnchargeQuote", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).GetUnchargeQuote(ctx, req.(*QuoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _SwapClient_serviceDesc = grpc.ServiceDesc{ + ServiceName: "rpc.SwapClient", + HandlerType: (*SwapClientServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Uncharge", + Handler: _SwapClient_Uncharge_Handler, + }, + { + MethodName: "GetUnchargeTerms", + Handler: _SwapClient_GetUnchargeTerms_Handler, + }, + { + MethodName: "GetUnchargeQuote", + Handler: _SwapClient_GetUnchargeQuote_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Monitor", + Handler: _SwapClient_Monitor_Handler, + ServerStreams: true, + }, + }, + Metadata: "swapclient.proto", +} + +func init() { proto.RegisterFile("swapclient.proto", fileDescriptor_swapclient_d9c5a6779b6644af) } + +var fileDescriptor_swapclient_d9c5a6779b6644af = []byte{ + // 744 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0x5d, 0x4f, 0xe3, 0x46, + 0x14, 0x25, 0xdf, 0xf1, 0x4d, 0xe2, 0x38, 0x03, 0xad, 0x02, 0x15, 0x55, 0xb0, 0x50, 0x9b, 0xf2, + 0x40, 0x5b, 0x78, 0xea, 0xa3, 0x9b, 0x18, 0x9a, 0x6a, 0x41, 0xec, 0x24, 0xd9, 0x57, 0x6b, 0x48, + 0x06, 0xb0, 0x94, 0xb1, 0xbd, 0x9e, 0x31, 0x24, 0xff, 0x69, 0x1f, 0xf7, 0x57, 0xad, 0xb4, 0xff, + 0x61, 0x35, 0x1f, 0x36, 0x09, 0xda, 0x7d, 0xb3, 0xce, 0x3d, 0xf7, 0x8c, 0xef, 0x99, 0x73, 0x07, + 0x1c, 0xfe, 0x42, 0x92, 0xc5, 0x2a, 0xa4, 0x91, 0x38, 0x4f, 0xd2, 0x58, 0xc4, 0xa8, 0x92, 0x26, + 0x0b, 0xf7, 0x73, 0x19, 0xba, 0xf3, 0x68, 0xf1, 0x44, 0xd2, 0x47, 0x8a, 0xe9, 0xc7, 0x8c, 0x72, + 0x81, 0x1c, 0xa8, 0x10, 0x26, 0xfa, 0xa5, 0x41, 0x69, 0x58, 0xc1, 0xf2, 0x13, 0x21, 0xa8, 0x2e, + 0x29, 0x17, 0xfd, 0xf2, 0xa0, 0x34, 0xb4, 0xb0, 0xfa, 0x46, 0x7f, 0xc2, 0x01, 0x23, 0xeb, 0x40, + 0xca, 0x06, 0x69, 0x9c, 0x89, 0x30, 0x7a, 0x0c, 0x1e, 0x28, 0xed, 0x57, 0x54, 0x5b, 0x8f, 0x91, + 0xf5, 0xf4, 0x85, 0x24, 0x58, 0x57, 0xae, 0x28, 0x45, 0x97, 0xf0, 0xb3, 0x6c, 0x48, 0x52, 0x9a, + 0x90, 0xcd, 0x4e, 0x4b, 0x55, 0xb5, 0xec, 0x33, 0xb2, 0xbe, 0x53, 0xc5, 0xad, 0xa6, 0x01, 0xb4, + 0x8b, 0x53, 0x24, 0xb5, 0xa6, 0xa8, 0x60, 0xd4, 0x25, 0xe3, 0x14, 0xec, 0x2d, 0x59, 0xf9, 0xe3, + 0x75, 0xc5, 0x69, 0x17, 0x72, 0x1e, 0x13, 0xc8, 0x85, 0x8e, 0x64, 0xb1, 0x30, 0xa2, 0xa9, 0x12, + 0x6a, 0x28, 0x52, 0x8b, 0x91, 0xf5, 0x8d, 0xc4, 0xa4, 0xd2, 0x1f, 0xe0, 0x64, 0xc6, 0x8a, 0x60, + 0xf1, 0x44, 0xa2, 0x88, 0xae, 0xfa, 0xcd, 0x41, 0x69, 0x58, 0xc5, 0xdd, 0x1c, 0x1f, 0x69, 0xd8, + 0xfd, 0x15, 0xda, 0x6a, 0x3a, 0xca, 0x93, 0x38, 0xe2, 0x14, 0xd9, 0x50, 0x0e, 0x97, 0xca, 0x31, + 0x0b, 0x97, 0xc3, 0xa5, 0xeb, 0x80, 0x7d, 0x13, 0x47, 0xa1, 0x88, 0x53, 0x63, 0xaa, 0xfb, 0xb5, + 0x04, 0x20, 0x5b, 0xa6, 0x82, 0x88, 0x8c, 0x7f, 0xc7, 0x63, 0x2d, 0x51, 0xce, 0x25, 0xd0, 0x09, + 0x54, 0xc5, 0x26, 0xd1, 0x7e, 0xda, 0x17, 0x9d, 0xf3, 0x34, 0x59, 0x9c, 0x4b, 0x81, 0xd9, 0x26, + 0xa1, 0x58, 0x95, 0xd0, 0x29, 0xd4, 0xb8, 0x20, 0x42, 0x1b, 0x68, 0x5f, 0xd8, 0x05, 0x47, 0x1e, + 0x42, 0xb1, 0x2e, 0xa2, 0xdf, 0xa1, 0x1b, 0x46, 0xa1, 0x08, 0x89, 0x08, 0xe3, 0x28, 0x10, 0x21, + 0xcb, 0x5d, 0xb4, 0x5f, 0xe1, 0x59, 0xc8, 0x28, 0x1a, 0x82, 0xb3, 0x22, 0x5c, 0x04, 0x59, 0xb2, + 0x24, 0x82, 0x6a, 0xa6, 0xf6, 0xd2, 0x96, 0xf8, 0x5c, 0xc1, 0x8a, 0x79, 0x02, 0xed, 0x27, 0xb1, + 0x5a, 0x04, 0x64, 0xb9, 0x4c, 0x29, 0xe7, 0xca, 0x4c, 0x0b, 0xb7, 0x24, 0xe6, 0x69, 0xc8, 0xb5, + 0xa1, 0x3d, 0xa3, 0x29, 0xe3, 0xf9, 0xfc, 0x9f, 0xca, 0xd0, 0x31, 0x80, 0xf1, 0xec, 0x0c, 0x7a, + 0xea, 0x5a, 0x13, 0xb2, 0x61, 0x34, 0x12, 0x81, 0x4a, 0x98, 0xb6, 0xb0, 0x2b, 0x0b, 0x77, 0x1a, + 0x1f, 0xcb, 0xb0, 0xb9, 0xd0, 0xc9, 0x23, 0x10, 0xdc, 0x13, 0x4e, 0x95, 0x4f, 0x15, 0xdc, 0xe2, + 0x3a, 0x04, 0xff, 0x12, 0x4e, 0x77, 0x38, 0xa9, 0x74, 0xa5, 0xb2, 0xc3, 0xc1, 0xd2, 0x8b, 0x63, + 0x80, 0xad, 0xa0, 0xe8, 0xdc, 0x59, 0x49, 0x91, 0x92, 0xdf, 0xa0, 0xcb, 0xc2, 0x48, 0xa7, 0x8d, + 0xb0, 0x38, 0x8b, 0x84, 0xb1, 0xaa, 0xc3, 0xc2, 0x48, 0x1a, 0xeb, 0x29, 0x50, 0xf1, 0xf2, 0x54, + 0x1a, 0x5e, 0xdd, 0xf0, 0x74, 0x30, 0x0d, 0xef, 0x18, 0x60, 0xb1, 0x12, 0xcf, 0xc1, 0x92, 0xae, + 0x04, 0x51, 0x2e, 0xd5, 0xb0, 0x25, 0x91, 0xb1, 0x04, 0xd0, 0x21, 0x34, 0xa5, 0x8c, 0x04, 0x54, + 0xd0, 0x6a, 0xb8, 0xc1, 0xc8, 0x7a, 0xb4, 0x12, 0xcf, 0xee, 0x00, 0xda, 0xef, 0xb3, 0x58, 0xfc, + 0x78, 0x27, 0xdd, 0x07, 0xe8, 0x18, 0x86, 0xf1, 0xf3, 0x10, 0x9a, 0xc5, 0x9a, 0x68, 0x5e, 0xc3, + 0x8c, 0xfe, 0x66, 0xec, 0xf2, 0xdb, 0xb1, 0x7f, 0x01, 0xeb, 0x75, 0x31, 0xb4, 0x6b, 0x4d, 0x66, + 0xb6, 0xe2, 0xac, 0x0f, 0xcd, 0x3c, 0x76, 0xa8, 0x0d, 0xcd, 0xf9, 0xed, 0xe8, 0x3f, 0x0f, 0x5f, + 0xfb, 0xce, 0xde, 0xd9, 0xff, 0x60, 0x15, 0x61, 0x43, 0x1d, 0xb0, 0x26, 0xb7, 0x93, 0xd9, 0xc4, + 0x9b, 0xf9, 0x63, 0x67, 0x0f, 0xfd, 0x04, 0xbd, 0x3b, 0xec, 0x4f, 0x6e, 0xbc, 0x6b, 0x3f, 0xc0, + 0xfe, 0x07, 0xdf, 0x7b, 0xe7, 0x8f, 0x9d, 0x12, 0x6a, 0x41, 0x63, 0x3a, 0x1f, 0x8d, 0xfc, 0xe9, + 0xd4, 0xa9, 0x20, 0x80, 0xfa, 0x95, 0x37, 0x91, 0x85, 0xea, 0xc5, 0x17, 0xb3, 0x1e, 0x23, 0xf5, + 0x42, 0xa1, 0x4b, 0x68, 0xe6, 0xaf, 0x12, 0x3a, 0x50, 0xb1, 0x7e, 0xf3, 0x48, 0x1d, 0xf5, 0x8a, + 0xb0, 0x17, 0x06, 0xfc, 0x0d, 0x0d, 0xb3, 0x74, 0x68, 0x5f, 0x55, 0x77, 0x57, 0xf0, 0xa8, 0xbb, + 0xb3, 0x1f, 0x19, 0xff, 0xab, 0x84, 0xfe, 0x01, 0xe7, 0x9a, 0x8a, 0x5c, 0x5b, 0xe5, 0x13, 0x69, + 0xe5, 0xed, 0xf0, 0x1e, 0xa1, 0x6d, 0xc8, 0x9c, 0xb6, 0xdb, 0xaa, 0xae, 0xc2, 0xb4, 0x6e, 0x5f, + 0x9c, 0x69, 0xdd, 0xb9, 0xa9, 0xfb, 0xba, 0x7a, 0x80, 0x2f, 0xbf, 0x05, 0x00, 0x00, 0xff, 0xff, + 0x4d, 0x46, 0x11, 0xa8, 0x94, 0x05, 0x00, 0x00, +} diff --git a/cmd/swapd/rpc/swapclient.proto b/cmd/swapd/rpc/swapclient.proto new file mode 100644 index 0000000..ce3b79a --- /dev/null +++ b/cmd/swapd/rpc/swapclient.proto @@ -0,0 +1,259 @@ +syntax = "proto3"; + +package rpc; + +message UnchargeRequest { + /** + Requested swap amount in sat. This does not include the swap and miner + fee. + */ + int64 amt = 1; + + /** + Base58 encoded destination address for the swap. + */ + string dest = 2; + + /** + Maximum off-chain fee in msat that may be paid for payment to the server. + This limit is applied during path finding. Typically this value is taken + from the response of the GetQuote call. + */ + int64 max_swap_routing_fee = 3; + + /** + Maximum off-chain fee in msat that may be paid for payment to the server. + This limit is applied during path finding. Typically this value is taken + from the response of the GetQuote call. + */ + int64 max_prepay_routing_fee = 4; + + /** + Maximum we are willing to pay the server for the swap. This value is not + disclosed in the swap initiation call, but if the server asks for a + higher fee, we abort the swap. Typically this value is taken from the + response of the GetQuote call. It includes the prepay amount. + */ + int64 max_swap_fee = 5; + + /** + Maximum amount of the swap fee that may be charged as a prepayment. + */ + int64 max_prepay_amt = 6; + + /** + Maximum in on-chain fees that we are willing to spent. If we want to + sweep the on-chain htlc and the fee estimate turns out higher than this + value, we cancel the swap. If the fee estimate is lower, we publish the + sweep tx. + + If the sweep tx isn't confirmed, we are forced to ratchet up fees until + it is swept. Possibly even exceeding max_miner_fee if we get close to the + htlc timeout. Because the initial publication revealed the preimage, we + have no other choice. The server may already have pulled the off-chain + htlc. Only when the fee becomes higher than the swap amount, we can only + wait for fees to come down and hope - if we are past the timeout - that + the server isn't publishing the revocation. + + max_miner_fee is typically taken from the response of the GetQuote call. + */ + int64 max_miner_fee = 7; + + /** + The channel to uncharge. If zero, the channel to uncharge is selected based + on the lowest routing fee for the swap payment to the server. + */ + uint64 uncharge_channel = 8; +} + + +message SwapResponse { + /** + Swap identifier to track status in the update stream that is returned from + the Start() call. Currently this is the hash that locks the htlcs. + */ + string id = 1; +} + +message MonitorRequest{ +} + + +message SwapStatus { + /** + Requested swap amount in sat. This does not include the swap and miner + fee. + */ + int64 amt = 1; + + /** + Swap identifier to track status in the update stream that is returned from + the Start() call. Currently this is the hash that locks the htlcs. + */ + string id = 2; + + /** + Swap type + */ + SwapType type = 3; + + /** + State the swap is currently in, see State enum. + */ + SwapState state = 4; + + /** + Initiation time of the swap. + */ + int64 initiation_time = 5; + + /** + Initiation time of the swap. + */ + int64 last_update_time = 6; + + /** + Htlc address. + */ + string htlc_address = 7; +} + +enum SwapType { + // UNCHARGE indicates an uncharge swap (off-chain to on-chain) + UNCHARGE = 0; + + +} + +enum SwapState { + /** + INITIATED is the initial state of a swap. At that point, the initiation + call to the server has been made and the payment process has been started + for the swap and prepayment invoices. + */ + INITIATED = 0; + + /** + PREIMAGE_REVEALED is reached when the sweep tx publication is first + attempted. From that point on, we should consider the preimage to no + longer be secret and we need to do all we can to get the sweep confirmed. + This state will mostly coalesce with StateHtlcConfirmed, except in the + case where we wait for fees to come down before we sweep. + */ + PREIMAGE_REVEALED = 1; + + /** + SUCCESS is the final swap state that is reached when the sweep tx has + the required confirmation depth. + */ + SUCCESS = 3; + + /** + FAILED is the final swap state for a failed swap with or without loss of + the swap amount. + */ + FAILED = 4; +} + +message TermsRequest { +} + +message TermsResponse { + /** + The node pubkey where the swap payment needs to be paid + to. This can be used to test connectivity before initiating the swap. + */ + string swap_payment_dest = 1; + + /** + The base fee for a swap (sat) + */ + int64 swap_fee_base = 2; + + /** + The fee rate for a swap (parts per million) + */ + int64 swap_fee_rate = 3; + + /** + Required prepay amount + */ + int64 prepay_amt = 4; + + /** + Minimum swap amount (sat) + */ + int64 min_swap_amount = 5; + + /** + Maximum swap amount (sat) + */ + int64 max_swap_amount = 6; + + /** + On-chain cltv expiry delta + */ + int32 cltv_delta = 7; + + /** + Maximum cltv expiry delta + */ + int32 max_cltv = 8; +} + +message QuoteRequest { + /** + Requested swap amount in sat. + */ + int64 amt = 1; +} + +message QuoteResponse { + /** + The fee that the swap server is charging for the swap. + */ + int64 swap_fee = 1; + + /** + The part of the swap fee that is requested as a + prepayment. + */ + int64 prepay_amt = 2; + + /** + An estimate of the on-chain fee that needs to be paid to + sweep the htlc. + */ + int64 miner_fee = 3; +} + +/** +SwapClient is a service that handles the client side process of onchain/offchain +swaps. The service is designed for a single client. +*/ +service SwapClient { + /** + Uncharge initiates an uncharge swap with the given parameters. The call + returns after the swap has been set up with the swap server. From that + point onwards, progress can be tracked via the SwapStatus stream + that is returned from Monitor(). + */ + rpc Uncharge(UnchargeRequest) returns (SwapResponse); + + + /** + Monitor will return a stream of swap updates for currently active swaps. + */ + rpc Monitor(MonitorRequest) returns(stream SwapStatus); + + /** + GetTerms returns the terms that the server enforces for swaps. + */ + rpc GetUnchargeTerms(TermsRequest) returns(TermsResponse); + + /** + GetQuote returns a quote for a swap with the provided parameters. + */ + rpc GetUnchargeQuote(QuoteRequest) returns(QuoteResponse); +} + diff --git a/cmd/swapd/swapclient_server.go b/cmd/swapd/swapclient_server.go new file mode 100644 index 0000000..553dead --- /dev/null +++ b/cmd/swapd/swapclient_server.go @@ -0,0 +1,247 @@ +package main + +import ( + "context" + "fmt" + "sort" + + "github.com/lightningnetwork/lnd/queue" + + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/utils" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/client" + clientrpc "github.com/lightninglabs/nautilus/cmd/swapd/rpc" +) + +const completedSwapsCount = 5 + +// swapClientServer implements the grpc service exposed by swapd. +type swapClientServer struct { + impl *client.Client + lnd *lndclient.LndServices +} + +// Uncharge initiates an uncharge swap with the given parameters. The call +// returns after the swap has been set up with the swap server. From that point +// onwards, progress can be tracked via the UnchargeStatus stream that is +// returned from Monitor(). +func (s *swapClientServer) Uncharge(ctx context.Context, + in *clientrpc.UnchargeRequest) ( + *clientrpc.SwapResponse, error) { + + logger.Infof("Uncharge request received") + + var sweepAddr btcutil.Address + if in.Dest == "" { + // Generate sweep address if none specified. + var err error + sweepAddr, err = s.lnd.WalletKit.NextAddr(context.Background()) + if err != nil { + return nil, fmt.Errorf("NextAddr error: %v", err) + } + } else { + var err error + sweepAddr, err = btcutil.DecodeAddress(in.Dest, nil) + if err != nil { + return nil, fmt.Errorf("decode address: %v", err) + } + } + + req := &client.UnchargeRequest{ + Amount: btcutil.Amount(in.Amt), + DestAddr: sweepAddr, + MaxMinerFee: btcutil.Amount(in.MaxMinerFee), + MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt), + MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee), + MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee), + MaxSwapFee: btcutil.Amount(in.MaxSwapFee), + SweepConfTarget: defaultConfTarget, + } + if in.UnchargeChannel != 0 { + req.UnchargeChannel = &in.UnchargeChannel + } + hash, err := s.impl.Uncharge(ctx, req) + if err != nil { + logger.Errorf("Uncharge: %v", err) + return nil, err + } + + return &clientrpc.SwapResponse{ + Id: hash.String(), + }, nil +} + +func (s *swapClientServer) marshallSwap(swap *client.SwapInfo) ( + *clientrpc.SwapStatus, error) { + + var state clientrpc.SwapState + switch swap.State { + case client.StateInitiated: + state = clientrpc.SwapState_INITIATED + case client.StatePreimageRevealed: + state = clientrpc.SwapState_PREIMAGE_REVEALED + case client.StateSuccess: + state = clientrpc.SwapState_SUCCESS + default: + // Return less granular status over rpc. + state = clientrpc.SwapState_FAILED + } + + htlc, err := utils.NewHtlc(swap.CltvExpiry, swap.SenderKey, + swap.ReceiverKey, swap.SwapHash, + ) + if err != nil { + return nil, err + } + + address, err := htlc.Address(s.lnd.ChainParams) + if err != nil { + return nil, err + } + + return &clientrpc.SwapStatus{ + Amt: int64(swap.AmountRequested), + Id: swap.SwapHash.String(), + State: state, + InitiationTime: swap.InitiationTime.UnixNano(), + LastUpdateTime: swap.LastUpdate.UnixNano(), + HtlcAddress: address.EncodeAddress(), + Type: clientrpc.SwapType_UNCHARGE, + }, nil +} + +// Monitor will return a stream of swap updates for currently active swaps. +func (s *swapClientServer) Monitor(in *clientrpc.MonitorRequest, + server clientrpc.SwapClient_MonitorServer) error { + + logger.Infof("Monitor request received") + + send := func(info client.SwapInfo) error { + rpcSwap, err := s.marshallSwap(&info) + if err != nil { + return err + } + + return server.Send(rpcSwap) + } + + // Start a notification queue for this subscriber. + queue := queue.NewConcurrentQueue(20) + queue.Start() + + // Add this subscriber to the global subscriber list. Also create a + // snapshot of all pending and completed swaps within the lock, to + // prevent subscribers from receiving duplicate updates. + swapsLock.Lock() + + id := nextSubscriberID + nextSubscriberID++ + subscribers[id] = queue.ChanIn() + + var pendingSwaps, completedSwaps []client.SwapInfo + for _, swap := range swaps { + if swap.State.Type() == client.StateTypePending { + pendingSwaps = append(pendingSwaps, swap) + } else { + completedSwaps = append(completedSwaps, swap) + } + } + + swapsLock.Unlock() + + defer func() { + queue.Stop() + swapsLock.Lock() + delete(subscribers, id) + swapsLock.Unlock() + }() + + // Sort completed swaps new to old. + sort.Slice(completedSwaps, func(i, j int) bool { + return completedSwaps[i].LastUpdate.After( + completedSwaps[j].LastUpdate, + ) + }) + + // Discard all but top x latest. + if len(completedSwaps) > completedSwapsCount { + completedSwaps = completedSwaps[:completedSwapsCount] + } + + // Concatenate both sets. + filteredSwaps := append(pendingSwaps, completedSwaps...) + + // Sort again, but this time old to new. + sort.Slice(filteredSwaps, func(i, j int) bool { + return filteredSwaps[i].LastUpdate.Before( + filteredSwaps[j].LastUpdate, + ) + }) + + // Return swaps to caller. + for _, swap := range filteredSwaps { + if err := send(swap); err != nil { + return err + } + } + + // As long as the client is connected, keep passing through swap + // updates. + for { + select { + case queueItem, ok := <-queue.ChanOut(): + if !ok { + return nil + } + + swap := queueItem.(client.SwapInfo) + if err := send(swap); err != nil { + return err + } + case <-server.Context().Done(): + return nil + } + } +} + +// GetTerms returns the terms that the server enforces for swaps. +func (s *swapClientServer) GetUnchargeTerms(ctx context.Context, req *clientrpc.TermsRequest) ( + *clientrpc.TermsResponse, error) { + + logger.Infof("Terms request received") + + terms, err := s.impl.UnchargeTerms(ctx) + if err != nil { + logger.Errorf("Terms request: %v", err) + return nil, err + } + + return &clientrpc.TermsResponse{ + MinSwapAmount: int64(terms.MinSwapAmount), + MaxSwapAmount: int64(terms.MaxSwapAmount), + PrepayAmt: int64(terms.PrepayAmt), + SwapFeeBase: int64(terms.SwapFeeBase), + SwapFeeRate: int64(terms.SwapFeeRate), + CltvDelta: int32(terms.CltvDelta), + }, nil +} + +// GetQuote returns a quote for a swap with the provided parameters. +func (s *swapClientServer) GetUnchargeQuote(ctx context.Context, + req *clientrpc.QuoteRequest) (*clientrpc.QuoteResponse, error) { + + quote, err := s.impl.UnchargeQuote(ctx, &client.UnchargeQuoteRequest{ + Amount: btcutil.Amount(req.Amt), + SweepConfTarget: defaultConfTarget, + }) + if err != nil { + return nil, err + } + return &clientrpc.QuoteResponse{ + MinerFee: int64(quote.MinerFee), + PrepayAmt: int64(quote.PrepayAmount), + SwapFee: int64(quote.SwapFee), + }, nil +} diff --git a/cmd/swapd/utils.go b/cmd/swapd/utils.go new file mode 100644 index 0000000..79501e3 --- /dev/null +++ b/cmd/swapd/utils.go @@ -0,0 +1,49 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/lightninglabs/nautilus/client" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/urfave/cli" +) + +// getLnd returns an instance of the lnd services proxy. +func getLnd(ctx *cli.Context) (*lndclient.GrpcLndServices, error) { + network := ctx.GlobalString("network") + + return lndclient.NewLndServices(ctx.GlobalString("lnd"), + "client", network, ctx.GlobalString("macaroonpath"), + ctx.GlobalString("tlspath"), + ) +} + +// getClient returns an instance of the swap client. +func getClient(ctx *cli.Context, lnd *lndclient.LndServices) (*client.Client, func(), error) { + network := ctx.GlobalString("network") + + storeDir, err := getStoreDir(network) + if err != nil { + return nil, nil, err + } + + swapClient, cleanUp, err := client.NewClient( + storeDir, ctx.GlobalString("swapserver"), + ctx.GlobalBool("insecure"), lnd, + ) + if err != nil { + return nil, nil, err + } + + return swapClient, cleanUp, nil +} + +func getStoreDir(network string) (string, error) { + dir := filepath.Join(defaultSwapletDir, network) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return "", err + } + + return dir, nil +} diff --git a/cmd/swapd/view.go b/cmd/swapd/view.go new file mode 100644 index 0000000..762013c --- /dev/null +++ b/cmd/swapd/view.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/lightninglabs/nautilus/utils" + "github.com/urfave/cli" +) + +var viewCommand = cli.Command{ + Name: "view", + Usage: `view all swaps in the database. This command can only be + executed when swapd is not running.`, + Description: ` + Show all pending and completed swaps.`, + Action: view, +} + +// view prints all swaps currently in the database. +func view(ctx *cli.Context) error { + network := ctx.GlobalString("network") + + chainParams, err := utils.ChainParamsFromNetwork(network) + if err != nil { + return err + } + + lnd, err := getLnd(ctx) + if err != nil { + return err + } + defer lnd.Close() + + swapClient, cleanup, err := getClient(ctx, &lnd.LndServices) + if err != nil { + return err + } + defer cleanup() + + swaps, err := swapClient.GetUnchargeSwaps() + if err != nil { + return err + } + + for _, s := range swaps { + htlc, err := utils.NewHtlc( + s.Contract.CltvExpiry, + s.Contract.SenderKey, + s.Contract.ReceiverKey, + s.Hash, + ) + if err != nil { + return err + } + + htlcAddress, err := htlc.Address(chainParams) + if err != nil { + return err + } + + fmt.Printf("%v\n", s.Hash) + fmt.Printf(" Created: %v (height %v)\n", + s.Contract.InitiationTime, s.Contract.InitiationHeight, + ) + fmt.Printf(" Preimage: %v\n", s.Contract.Preimage) + fmt.Printf(" Htlc address: %v\n", htlcAddress) + + unchargeChannel := "any" + if s.Contract.UnchargeChannel != nil { + unchargeChannel = strconv.FormatUint( + *s.Contract.UnchargeChannel, 10, + ) + } + fmt.Printf(" Uncharge channel: %v\n", unchargeChannel) + fmt.Printf(" Dest: %v\n", s.Contract.DestAddr) + fmt.Printf(" Amt: %v, Expiry: %v\n", + s.Contract.AmountRequested, s.Contract.CltvExpiry, + ) + for i, e := range s.Events { + fmt.Printf(" Update %v, Time %v, State: %v\n", + i, e.Time, e.State, + ) + } + fmt.Println() + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..10c037e --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module github.com/lightninglabs/nautilus + +require ( + github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589 + github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820 + github.com/coreos/etcd v3.3.12+incompatible + github.com/coreos/go-semver v0.2.0 // indirect + github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect + github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/fortytw2/leaktest v1.3.0 + github.com/ghodss/yaml v1.0.0 // indirect + github.com/gogo/protobuf v1.2.0 // indirect + github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect + github.com/golang/protobuf v1.2.0 + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect + github.com/gorilla/websocket v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19 + github.com/jonboulle/clockwork v0.1.0 // indirect + github.com/lightningnetwork/lnd v0.0.0 + github.com/pkg/errors v0.8.0 // indirect + github.com/prometheus/client_golang v0.9.2 // indirect + github.com/sirupsen/logrus v1.2.0 // indirect + github.com/soheilhy/cmux v0.1.4 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect + github.com/ugorji/go v1.1.1 // indirect + github.com/urfave/cli v1.20.0 + github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect + go.etcd.io/etcd v3.3.12+incompatible + go.uber.org/atomic v1.3.2 // indirect + go.uber.org/multierr v1.1.0 // indirect + go.uber.org/zap v1.9.1 // indirect + golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 + golang.org/x/net v0.0.0-20181201002055-351d144fa1fc + google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a + google.golang.org/grpc v1.16.0 + gopkg.in/macaroon.v2 v2.0.0 +) + +replace github.com/lightningnetwork/lnd => github.com/joostjager/lnd v0.4.1-beta.0.20190204111535-7159d0a5ef0a diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a79d6be --- /dev/null +++ b/go.sum @@ -0,0 +1,240 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= +github.com/NebulousLabs/fastrand v0.0.0-20180208210444-3cf7173006a0/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= +github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= +github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/btcsuite/btcd v0.0.0-20180823030728-d81d8877b8f3/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= +github.com/btcsuite/btcd v0.0.0-20180824064422-ed77733ec07dfc8a513741138419b8d9d3de9d2d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589 h1:9A5pe5iQS+ll6R1EVLFv/y92IjrymihwITCU81aCIBQ= +github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet v0.0.0-20180904010540-284e2e0e696e33d5be388f7f3d9a26db703e0c06/go.mod h1:/d7QHZsfUAruXuBhyPITqoYOmJ+nq35qPsJjz/aSpCg= +github.com/btcsuite/btcwallet v0.0.0-20190123033236-ba03278a64bc h1:E7lDde/zAxAfvF750wMP0pUIAzF+wtwO2jQRy++q60U= +github.com/btcsuite/btcwallet v0.0.0-20190123033236-ba03278a64bc/go.mod h1:+u1ftn+QOb9qHKwsLf7rBOr0PHCo9CGA7U1WFq7VLA4= +github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY= +github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8 h1:nOsAWScwueMVk/VLm/dvQQD7DuanyvAUb6B3P3eT274= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820 h1:W1bWzjKRrqKEpWlFsJ6Yef9Q4LUhdfJmS6sQrQj5L6c= +github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.12+incompatible h1:pAWNwdf7QiT1zfaWyqCtNZQWCLByQyA3JrSQyuYAqnQ= +github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk= +github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff h1:kOkM9whyQYodu09SJ6W3NCsHG7crFaJILQ22Gozp3lg= +github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20180821051752-b27b920f9e71/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc h1:3NXdOHZ1YlN6SGP3FPbn4k73O2MeEp065abehRwGFxI= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackpal/gateway v1.0.4/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= +github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19 h1:k9/LaykApavRKKlaWkunBd48Um+vMxnUNNsIjS7OJn8= +github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/joostjager/lnd v0.4.1-beta.0.20190204111535-7159d0a5ef0a h1:AExcTWAjSQSk7w94Hc15xPSTiLTft82xnAbe52NpQW0= +github.com/joostjager/lnd v0.4.1-beta.0.20190204111535-7159d0a5ef0a/go.mod h1:4axuRDteyNJN9JOK1yxIvRhtNNiWvshXk9eMnBxhbCk= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/juju/clock v0.0.0-20180808021310-bab88fc67299 h1:K9nBHQ3UNqg/HhZkQnGG2AE4YxDyNmGS9FFT2gGegLQ= +github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20180524022052-584905176618 h1:MK144iBQF9hTSwBW/9eJm034bVoG30IshVm688T2hi8= +github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 h1:WQM1NildKThwdP7qWrNAFGzp4ijNLw8RlgENkaI4MJs= +github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY= +github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag= +github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885 h1:fTLuPUkaKIIV0+gA1IxiBDvDxtF8tzpSF6N6NfFGmsU= +github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885/go.mod h1:KUh15naRlx/TmUMFS/p4JJrCrE6F7RGF7rsnvuu45E4= +github.com/lightninglabs/neutrino v0.0.0-20181017011010-4d6069299130/go.mod h1:KJq43Fu9ceitbJsSXMILcT4mGDNI/crKmPIkDOZXFyM= +github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af h1:JzoYbWqwPb+PARU4LTtlohetdNa6/ocyQ0xidZQw4Hg= +github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af/go.mod h1:aR+E6cs+FTaIwIa/WLyvNsB8FZg8TiP3r0Led+4Q4gI= +github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6 h1:ONLGrYJVQdbtP6CE/ff1KNWZtygRGEh12RzonTiCzPs= +github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6/go.mod h1:8EgEt4a/NUOVQd+3kk6n9aZCJ1Ssj96Pb6lCrci+6oc= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= +github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc= +github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= +github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= +github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA= +github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= +github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 h1:MPPkRncZLN9Kh4MEFmbnK4h3BD7AUmskWv2+EeZJCCs= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.0 h1:oY10fI923Q5pVCVt1GBTZMn8LHo5M+RCInFpeMnV4QI= +go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v3.3.12+incompatible h1:V6PRYRGpU4k5EajJaaj/GL3hqIdzyPnBU8aPUp+35yw= +go.etcd.io/etcd v3.3.12+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a h1:Weemm+oF2juintSvD0c+ZG4lDmCwgYKrM/kPI6gFINY= +google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v1 v1.0.0 h1:n+7XfCyygBFb8sEjg6692xjC6Us50TFRO54+xYUEwjE= +gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/macaroon-bakery.v2 v2.0.1 h1:0N1TlEdfLP4HXNCg7MQUMp5XwvOoxk+oe9Owr2cpvsc= +gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= +gopkg.in/macaroon.v2 v2.0.0 h1:LVWycAfeJBUjCIqfR9gqlo7I8vmiXRr51YEOZ1suop8= +gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/lndclient/chainnotifier_client.go b/lndclient/chainnotifier_client.go new file mode 100644 index 0000000..ce86b76 --- /dev/null +++ b/lndclient/chainnotifier_client.go @@ -0,0 +1,244 @@ +package lndclient + +import ( + "context" + "fmt" + "sync" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnrpc/chainrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// ChainNotifierClient exposes base lightning functionality. +type ChainNotifierClient interface { + RegisterBlockEpochNtfn(ctx context.Context) ( + chan int32, chan error, error) + + RegisterConfirmationsNtfn(ctx context.Context, txid *chainhash.Hash, + pkScript []byte, numConfs, heightHint int32) ( + chan *chainntnfs.TxConfirmation, chan error, error) + + RegisterSpendNtfn(ctx context.Context, + outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( + chan *chainntnfs.SpendDetail, chan error, error) +} + +type chainNotifierClient struct { + client chainrpc.ChainNotifierClient + wg sync.WaitGroup +} + +func newChainNotifierClient(conn *grpc.ClientConn) *chainNotifierClient { + return &chainNotifierClient{ + client: chainrpc.NewChainNotifierClient(conn), + } +} + +func (s *chainNotifierClient) WaitForFinished() { + s.wg.Wait() +} + +func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context, + outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( + chan *chainntnfs.SpendDetail, chan error, error) { + + var rpcOutpoint *chainrpc.Outpoint + if outpoint != nil { + rpcOutpoint = &chainrpc.Outpoint{ + Hash: outpoint.Hash[:], + Index: outpoint.Index, + } + } + + resp, err := s.client.RegisterSpendNtfn(ctx, &chainrpc.SpendRequest{ + HeightHint: uint32(heightHint), + Outpoint: rpcOutpoint, + Script: pkScript, + }) + if err != nil { + return nil, nil, err + } + + spendChan := make(chan *chainntnfs.SpendDetail, 1) + errChan := make(chan error, 1) + + processSpendDetail := func(d *chainrpc.SpendDetails) error { + outpointHash, err := chainhash.NewHash(d.SpendingOutpoint.Hash) + if err != nil { + return err + } + txHash, err := chainhash.NewHash(d.SpendingTxHash) + if err != nil { + return err + } + tx, err := utils.DecodeTx(d.RawSpendingTx) + if err != nil { + return err + } + spendChan <- &chainntnfs.SpendDetail{ + SpentOutPoint: &wire.OutPoint{ + Hash: *outpointHash, + Index: d.SpendingOutpoint.Index, + }, + SpenderTxHash: txHash, + SpenderInputIndex: d.SpendingInputIndex, + SpendingTx: tx, + SpendingHeight: int32(d.SpendingHeight), + } + + return nil + } + + s.wg.Add(1) + go func() { + defer s.wg.Done() + for { + spendEvent, err := resp.Recv() + if err != nil { + if status.Code(err) != codes.Canceled { + errChan <- err + } + return + } + + switch c := spendEvent.Event.(type) { + case *chainrpc.SpendEvent_Spend: + err := processSpendDetail(c.Spend) + if err != nil { + errChan <- err + } + return + } + } + }() + + return spendChan, errChan, nil +} + +func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context, + txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) ( + chan *chainntnfs.TxConfirmation, chan error, error) { + + // TODO: Height hint + var txidSlice []byte + if txid != nil { + txidSlice = txid[:] + } + confStream, err := s.client. + RegisterConfirmationsNtfn( + ctx, + &chainrpc.ConfRequest{ + Script: pkScript, + NumConfs: uint32(numConfs), + HeightHint: uint32(heightHint), + Txid: txidSlice, + }, + ) + if err != nil { + return nil, nil, err + } + + confChan := make(chan *chainntnfs.TxConfirmation, 1) + errChan := make(chan error, 1) + + s.wg.Add(1) + go func() { + defer s.wg.Done() + + for { + var confEvent *chainrpc.ConfEvent + confEvent, err := confStream.Recv() + if err != nil { + if status.Code(err) != codes.Canceled { + errChan <- err + } + return + } + + switch c := confEvent.Event.(type) { + + // Script confirmed + case *chainrpc.ConfEvent_Conf: + tx, err := utils.DecodeTx(c.Conf.RawTx) + if err != nil { + errChan <- err + return + } + blockHash, err := chainhash.NewHash( + c.Conf.BlockHash, + ) + if err != nil { + errChan <- err + return + } + confChan <- &chainntnfs.TxConfirmation{ + BlockHeight: c.Conf.BlockHeight, + BlockHash: blockHash, + Tx: tx, + TxIndex: c.Conf.TxIndex, + } + return + + // Ignore reorg events, not supported. + case *chainrpc.ConfEvent_Reorg: + continue + + // Nil event, should never happen. + case nil: + errChan <- fmt.Errorf("conf event empty") + return + + // Unexpected type. + default: + errChan <- fmt.Errorf( + "conf event has unexpected type", + ) + return + } + } + }() + + return confChan, errChan, nil +} + +func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) ( + chan int32, chan error, error) { + + blockEpochClient, err := s.client. + RegisterBlockEpochNtfn(ctx, &chainrpc.BlockEpoch{}) + if err != nil { + return nil, nil, err + } + + blockErrorChan := make(chan error, 1) + blockEpochChan := make(chan int32) + + // Start block epoch goroutine. + s.wg.Add(1) + go func() { + defer s.wg.Done() + for { + epoch, err := blockEpochClient.Recv() + if err != nil { + if status.Code(err) != codes.Canceled { + blockErrorChan <- err + } + return + } + + select { + case blockEpochChan <- int32(epoch.Height): + case <-ctx.Done(): + return + } + } + }() + + return blockEpochChan, blockErrorChan, nil +} diff --git a/lndclient/invoices_client.go b/lndclient/invoices_client.go new file mode 100644 index 0000000..b70c2f8 --- /dev/null +++ b/lndclient/invoices_client.go @@ -0,0 +1,154 @@ +package lndclient + +import ( + "context" + "errors" + "sync" + + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lntypes" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// InvoicesClient exposes invoice functionality. +type InvoicesClient interface { + SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) ( + <-chan channeldb.ContractState, <-chan error, error) + + SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error + + CancelInvoice(ctx context.Context, hash lntypes.Hash) error + + AddHoldInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) ( + string, error) +} + +type invoicesClient struct { + client invoicesrpc.InvoicesClient + wg sync.WaitGroup +} + +func newInvoicesClient(conn *grpc.ClientConn) *invoicesClient { + return &invoicesClient{ + client: invoicesrpc.NewInvoicesClient(conn), + } +} + +func (s *invoicesClient) WaitForFinished() { + s.wg.Wait() +} + +func (s *invoicesClient) SettleInvoice(ctx context.Context, + preimage lntypes.Preimage) error { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + _, err := s.client.SettleInvoice(rpcCtx, &invoicesrpc.SettleInvoiceMsg{ + PreImage: preimage[:], + }) + + return err +} + +func (s *invoicesClient) CancelInvoice(ctx context.Context, + hash lntypes.Hash) error { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + _, err := s.client.CancelInvoice(rpcCtx, &invoicesrpc.CancelInvoiceMsg{ + PaymentHash: hash[:], + }) + + return err +} + +func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, + hash lntypes.Hash) (<-chan channeldb.ContractState, + <-chan error, error) { + + invoiceStream, err := s.client. + SubscribeSingleInvoice(ctx, + &lnrpc.PaymentHash{ + RHash: hash[:], + }) + if err != nil { + return nil, nil, err + } + + updateChan := make(chan channeldb.ContractState) + errChan := make(chan error, 1) + + // Invoice updates goroutine. + s.wg.Add(1) + go func() { + defer s.wg.Done() + for { + invoice, err := invoiceStream.Recv() + if err != nil { + if status.Code(err) != codes.Canceled { + errChan <- err + } + return + } + + state, err := fromRPCInvoiceState(invoice.State) + if err != nil { + errChan <- err + return + } + + select { + case updateChan <- state: + case <-ctx.Done(): + return + } + } + }() + + return updateChan, errChan, nil +} + +func (s *invoicesClient) AddHoldInvoice(ctx context.Context, + in *invoicesrpc.AddInvoiceData) (string, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + rpcIn := &invoicesrpc.AddHoldInvoiceRequest{ + Memo: in.Memo, + Hash: in.Hash[:], + Value: int64(in.Value), + Expiry: in.Expiry, + CltvExpiry: in.CltvExpiry, + Private: true, + } + + resp, err := s.client.AddHoldInvoice(rpcCtx, rpcIn) + if err != nil { + return "", err + } + return resp.PaymentRequest, nil +} + +func fromRPCInvoiceState(state lnrpc.Invoice_InvoiceState) ( + channeldb.ContractState, error) { + + switch state { + case lnrpc.Invoice_OPEN: + return channeldb.ContractOpen, nil + case lnrpc.Invoice_ACCEPTED: + return channeldb.ContractAccepted, nil + case lnrpc.Invoice_SETTLED: + return channeldb.ContractSettled, nil + case lnrpc.Invoice_CANCELED: + return channeldb.ContractCanceled, nil + } + + return 0, errors.New("unknown state") +} diff --git a/lndclient/lightning_client.go b/lndclient/lightning_client.go new file mode 100644 index 0000000..36c14ae --- /dev/null +++ b/lndclient/lightning_client.go @@ -0,0 +1,330 @@ +package lndclient + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// LightningClient exposes base lightning functionality. +type LightningClient interface { + PayInvoice(ctx context.Context, invoice string, + maxFee btcutil.Amount, + outgoingChannel *uint64) chan PaymentResult + + GetInfo(ctx context.Context) (*Info, error) + + GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) ( + lnwire.MilliSatoshi, error) + + ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error) + + AddInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) ( + lntypes.Hash, string, error) +} + +// Info contains info about the connected lnd node. +type Info struct { + BlockHeight uint32 + IdentityPubkey [33]byte + Alias string + Network string +} + +var ( + // ErrMalformedServerResponse is returned when the swap and/or prepay + // invoice is malformed. + ErrMalformedServerResponse = errors.New( + "one or more invoices are malformed", + ) + + // ErrNoRouteToServer is returned if no quote can returned because there + // is no route to the server. + ErrNoRouteToServer = errors.New("no off-chain route to server") + + // PaymentResultUnknownPaymentHash is the string result returned by + // SendPayment when the final node indicates the hash is unknown. + PaymentResultUnknownPaymentHash = "UnknownPaymentHash" + + // PaymentResultSuccess is the string result returned by SendPayment + // when the payment was successful. + PaymentResultSuccess = "" + + // PaymentResultAlreadyPaid is the string result returned by SendPayment + // when the payment was already completed in a previous SendPayment + // call. + PaymentResultAlreadyPaid = htlcswitch.ErrAlreadyPaid.Error() + + // PaymentResultInFlight is the string result returned by SendPayment + // when the payment was initiated in a previous SendPayment call and + // still in flight. + PaymentResultInFlight = htlcswitch.ErrPaymentInFlight.Error() + + paymentPollInterval = 3 * time.Second +) + +type lightningClient struct { + client lnrpc.LightningClient + wg sync.WaitGroup + params *chaincfg.Params +} + +func newLightningClient(conn *grpc.ClientConn, + params *chaincfg.Params) *lightningClient { + + return &lightningClient{ + client: lnrpc.NewLightningClient(conn), + params: params, + } +} + +// PaymentResult signals the result of a payment. +type PaymentResult struct { + Err error + Preimage lntypes.Preimage + PaidFee btcutil.Amount + PaidAmt btcutil.Amount +} + +func (s *lightningClient) WaitForFinished() { + s.wg.Wait() +} + +func (s *lightningClient) ConfirmedWalletBalance(ctx context.Context) ( + btcutil.Amount, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := s.client.WalletBalance(rpcCtx, &lnrpc.WalletBalanceRequest{}) + if err != nil { + return 0, err + } + + return btcutil.Amount(resp.ConfirmedBalance), nil +} + +func (s *lightningClient) GetInfo(ctx context.Context) (*Info, error) { + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := s.client.GetInfo(rpcCtx, &lnrpc.GetInfoRequest{}) + if err != nil { + return nil, err + } + + pubKey, err := hex.DecodeString(resp.IdentityPubkey) + if err != nil { + return nil, err + } + + var pubKeyArray [33]byte + copy(pubKeyArray[:], pubKey) + + return &Info{ + BlockHeight: resp.BlockHeight, + IdentityPubkey: pubKeyArray, + Alias: resp.Alias, + Network: resp.Chains[0].Network, + }, nil +} + +func (s *lightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, + dest [33]byte) (lnwire.MilliSatoshi, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + routeResp, err := s.client.QueryRoutes( + rpcCtx, + &lnrpc.QueryRoutesRequest{ + Amt: int64(amt), + NumRoutes: 1, + PubKey: hex.EncodeToString(dest[:]), + }, + ) + if err != nil { + return 0, err + } + if len(routeResp.Routes) == 0 { + return 0, ErrNoRouteToServer + } + + return lnwire.MilliSatoshi(routeResp.Routes[0].TotalFeesMsat), nil +} + +// PayInvoice pays an invoice. +func (s *lightningClient) PayInvoice(ctx context.Context, invoice string, + maxFee btcutil.Amount, outgoingChannel *uint64) chan PaymentResult { + + // Use buffer to prevent blocking. + paymentChan := make(chan PaymentResult, 1) + + // Execute payment in parallel, because it will block until server + // discovers preimage. + s.wg.Add(1) + go func() { + defer s.wg.Done() + + result := s.payInvoice(ctx, invoice, maxFee, outgoingChannel) + if result != nil { + paymentChan <- *result + } + }() + + return paymentChan +} + +// payInvoice tries to send a payment and returns the final result. If +// necessary, it will poll lnd for the payment result. +func (s *lightningClient) payInvoice(ctx context.Context, invoice string, + maxFee btcutil.Amount, outgoingChannel *uint64) *PaymentResult { + + payReq, err := zpay32.Decode(invoice, s.params) + if err != nil { + return &PaymentResult{ + Err: fmt.Errorf("invoice decode: %v", err), + } + } + + if payReq.MilliSat == nil { + return &PaymentResult{ + Err: errors.New("no amount in invoice"), + } + } + + hash := lntypes.Hash(*payReq.PaymentHash) + + for { + // Create no timeout context as this call can block for a long + // time. + + req := &lnrpc.SendRequest{ + FeeLimit: &lnrpc.FeeLimit{ + Limit: &lnrpc.FeeLimit_Fixed{ + Fixed: int64(maxFee), + }, + }, + PaymentRequest: invoice, + } + + if outgoingChannel != nil { + req.OutgoingChannelID = *outgoingChannel + } + + payResp, err := s.client.SendPaymentSync(ctx, req) + + if status.Code(err) == codes.Canceled { + return nil + } + + if err == nil { + // TODO: Use structured payment error when available, + // instead of this britle string matching. + switch payResp.PaymentError { + + // Paid successfully. + case PaymentResultSuccess: + logger.Infof( + "Payment %v completed", hash, + ) + + r := payResp.PaymentRoute + preimage, err := lntypes.NewPreimage( + payResp.PaymentPreimage, + ) + if err != nil { + return &PaymentResult{Err: err} + } + return &PaymentResult{ + PaidFee: btcutil.Amount(r.TotalFees), + PaidAmt: btcutil.Amount( + r.TotalAmt - r.TotalFees, + ), + Preimage: *preimage, + } + + // Invoice was already paid on a previous run. + case PaymentResultAlreadyPaid: + logger.Infof( + "Payment %v already completed", hash, + ) + + // Unfortunately lnd doesn't return the route if + // the payment was successful in a previous + // call. Assume paid fees 0 and take paid amount + // from invoice. + + return &PaymentResult{ + PaidFee: 0, + PaidAmt: payReq.MilliSat.ToSatoshis(), + } + + // If the payment is already in flight, we will poll + // again later for an outcome. + // + // TODO: Improve this when lnd expose more API to + // tracking existing payments. + case PaymentResultInFlight: + logger.Infof( + "Payment %v already in flight", hash, + ) + + time.Sleep(paymentPollInterval) + + // Other errors are transformed into an error struct. + default: + logger.Warnf( + "Payment %v failed: %v", hash, + payResp.PaymentError, + ) + + return &PaymentResult{ + Err: errors.New(payResp.PaymentError), + } + } + } + } +} + +func (s *lightningClient) AddInvoice(ctx context.Context, + in *invoicesrpc.AddInvoiceData) (lntypes.Hash, string, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + rpcIn := &lnrpc.Invoice{ + Memo: in.Memo, + RHash: in.Hash[:], + Value: int64(in.Value), + Expiry: in.Expiry, + CltvExpiry: in.CltvExpiry, + Private: true, + } + + resp, err := s.client.AddInvoice(rpcCtx, rpcIn) + if err != nil { + return lntypes.Hash{}, "", err + } + hash, err := lntypes.NewHash(resp.RHash) + if err != nil { + return lntypes.Hash{}, "", err + } + + return *hash, resp.PaymentRequest, nil +} diff --git a/lndclient/lnd_services.go b/lndclient/lnd_services.go new file mode 100644 index 0000000..146f6f6 --- /dev/null +++ b/lndclient/lnd_services.go @@ -0,0 +1,186 @@ +package lndclient + +import ( + "context" + "errors" + "fmt" + "github.com/lightninglabs/nautilus/utils" + "io/ioutil" + "path/filepath" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/macaroons" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + macaroon "gopkg.in/macaroon.v2" +) + +var rpcTimeout = 30 * time.Second + +// LndServices constitutes a set of required services. +type LndServices struct { + Client LightningClient + WalletKit WalletKitClient + ChainNotifier ChainNotifierClient + Signer SignerClient + Invoices InvoicesClient + + ChainParams *chaincfg.Params +} + +// GrpcLndServices constitutes a set of required RPC services. +type GrpcLndServices struct { + LndServices + + cleanup func() +} + +// NewLndServices creates a set of required RPC services. +func NewLndServices(lndAddress string, application string, + network string, macPath, tlsPath string) ( + *GrpcLndServices, error) { + + // Setup connection with lnd + logger.Infof("Creating lnd connection") + conn, err := getClientConn(lndAddress, network, macPath, tlsPath) + if err != nil { + return nil, err + } + + logger.Infof("Connected to lnd") + + chainParams, err := utils.ChainParamsFromNetwork(network) + if err != nil { + return nil, err + } + + lightningClient := newLightningClient(conn, chainParams) + + info, err := lightningClient.GetInfo(context.Background()) + if err != nil { + conn.Close() + return nil, err + } + if network != info.Network { + conn.Close() + return nil, errors.New( + "network mismatch with connected lnd instance", + ) + } + + notifierClient := newChainNotifierClient(conn) + signerClient := newSignerClient(conn) + walletKitClient := newWalletKitClient(conn) + invoicesClient := newInvoicesClient(conn) + + cleanup := func() { + logger.Debugf("Closing lnd connection") + conn.Close() + + logger.Debugf("Wait for client to finish") + lightningClient.WaitForFinished() + + logger.Debugf("Wait for chain notifier to finish") + notifierClient.WaitForFinished() + + logger.Debugf("Wait for invoices to finish") + invoicesClient.WaitForFinished() + + logger.Debugf("Lnd services finished") + } + + services := &GrpcLndServices{ + LndServices: LndServices{ + Client: lightningClient, + WalletKit: walletKitClient, + ChainNotifier: notifierClient, + Signer: signerClient, + Invoices: invoicesClient, + ChainParams: chainParams, + }, + cleanup: cleanup, + } + + logger.Infof("Using network %v", network) + + return services, nil +} + +// Close closes the lnd connection and waits for all sub server clients to +// finish their goroutines. +func (s *GrpcLndServices) Close() { + s.cleanup() + + logger.Debugf("Lnd services finished") +} + +var ( + defaultRPCPort = "10009" + defaultLndDir = btcutil.AppDataDir("lnd", false) + defaultTLSCertFilename = "tls.cert" + defaultTLSCertPath = filepath.Join(defaultLndDir, + defaultTLSCertFilename) + defaultDataDir = "data" + defaultChainSubDir = "chain" + defaultMacaroonFilename = "admin.macaroon" +) + +func getClientConn(address string, network string, macPath, tlsPath string) ( + *grpc.ClientConn, error) { + + // Load the specified TLS certificate and build transport credentials + // with it. + if tlsPath == "" { + tlsPath = defaultTLSCertPath + } + + creds, err := credentials.NewClientTLSFromFile(tlsPath, "") + if err != nil { + return nil, err + } + + // Create a dial options array. + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(creds), + } + + if macPath == "" { + macPath = filepath.Join( + defaultLndDir, defaultDataDir, defaultChainSubDir, + "bitcoin", network, defaultMacaroonFilename, + ) + } + + // Load the specified macaroon file. + macBytes, err := ioutil.ReadFile(macPath) + if err == nil { + // Only if file is found + mac := &macaroon.Macaroon{} + if err = mac.UnmarshalBinary(macBytes); err != nil { + return nil, fmt.Errorf("unable to decode macaroon: %v", + err) + } + + // Now we append the macaroon credentials to the dial options. + cred := macaroons.NewMacaroonCredential(mac) + opts = append(opts, grpc.WithPerRPCCredentials(cred)) + } + + // We need to use a custom dialer so we can also connect to unix sockets + // and not just TCP addresses. + opts = append( + opts, grpc.WithDialer( + lncfg.ClientAddressDialer(defaultRPCPort), + ), + ) + conn, err := grpc.Dial(address, opts...) + if err != nil { + return nil, fmt.Errorf("unable to connect to RPC server: %v", err) + } + + return conn, nil +} diff --git a/lndclient/log.go b/lndclient/log.go new file mode 100644 index 0000000..d972f21 --- /dev/null +++ b/lndclient/log.go @@ -0,0 +1,23 @@ +package lndclient + +import ( + "github.com/btcsuite/btclog" + "os" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var ( + backendLog = btclog.NewBackend(logWriter{}) + logger = backendLog.Logger("LNDCLIENT") +) + +// logWriter implements an io.Writer that outputs to both standard output and +// the write-end pipe of an initialized log rotator. +type logWriter struct{} + +func (logWriter) Write(p []byte) (n int, err error) { + os.Stdout.Write(p) + return len(p), nil +} diff --git a/lndclient/signer_client.go b/lndclient/signer_client.go new file mode 100644 index 0000000..39d5ae1 --- /dev/null +++ b/lndclient/signer_client.go @@ -0,0 +1,90 @@ +package lndclient + +import ( + "context" + + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnrpc/signrpc" + "google.golang.org/grpc" +) + +// SignerClient exposes sign functionality. +type SignerClient interface { + SignOutputRaw(ctx context.Context, tx *wire.MsgTx, + signDescriptors []*input.SignDescriptor) ([][]byte, error) +} + +type signerClient struct { + client signrpc.SignerClient +} + +func newSignerClient(conn *grpc.ClientConn) *signerClient { + return &signerClient{ + client: signrpc.NewSignerClient(conn), + } +} + +func (s *signerClient) SignOutputRaw(ctx context.Context, tx *wire.MsgTx, + signDescriptors []*input.SignDescriptor) ([][]byte, error) { + + txRaw, err := utils.EncodeTx(tx) + if err != nil { + return nil, err + } + + rpcSignDescs := make([]*signrpc.SignDescriptor, len(signDescriptors)) + for i, signDesc := range signDescriptors { + var keyBytes []byte + var keyLocator *signrpc.KeyLocator + if signDesc.KeyDesc.PubKey != nil { + keyBytes = signDesc.KeyDesc.PubKey.SerializeCompressed() + } else { + keyLocator = &signrpc.KeyLocator{ + KeyFamily: int32( + signDesc.KeyDesc.KeyLocator.Family, + ), + KeyIndex: int32( + signDesc.KeyDesc.KeyLocator.Index, + ), + } + } + + var doubleTweak []byte + if signDesc.DoubleTweak != nil { + doubleTweak = signDesc.DoubleTweak.Serialize() + } + + rpcSignDescs[i] = &signrpc.SignDescriptor{ + WitnessScript: signDesc.WitnessScript, + Output: &signrpc.TxOut{ + PkScript: signDesc.Output.PkScript, + Value: signDesc.Output.Value, + }, + Sighash: uint32(signDesc.HashType), + InputIndex: int32(signDesc.InputIndex), + KeyDesc: &signrpc.KeyDescriptor{ + RawKeyBytes: keyBytes, + KeyLoc: keyLocator, + }, + SingleTweak: signDesc.SingleTweak, + DoubleTweak: doubleTweak, + } + } + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := s.client.SignOutputRaw(rpcCtx, + &signrpc.SignReq{ + RawTxBytes: txRaw, + SignDescs: rpcSignDescs, + }, + ) + if err != nil { + return nil, err + } + + return resp.RawSigs, nil +} diff --git a/lndclient/walletkit_client.go b/lndclient/walletkit_client.go new file mode 100644 index 0000000..595ab03 --- /dev/null +++ b/lndclient/walletkit_client.go @@ -0,0 +1,180 @@ +package lndclient + +import ( + "context" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc/signrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lnwallet" + "google.golang.org/grpc" +) + +// WalletKitClient exposes wallet functionality. +type WalletKitClient interface { + DeriveNextKey(ctx context.Context, family int32) ( + *keychain.KeyDescriptor, error) + + DeriveKey(ctx context.Context, locator *keychain.KeyLocator) ( + *keychain.KeyDescriptor, error) + + NextAddr(ctx context.Context) (btcutil.Address, error) + + PublishTransaction(ctx context.Context, tx *wire.MsgTx) error + + SendOutputs(ctx context.Context, outputs []*wire.TxOut, + feeRate lnwallet.SatPerKWeight) (*wire.MsgTx, error) + + EstimateFee(ctx context.Context, confTarget int32) (lnwallet.SatPerKWeight, + error) +} + +type walletKitClient struct { + client walletrpc.WalletKitClient +} + +func newWalletKitClient(conn *grpc.ClientConn) *walletKitClient { + return &walletKitClient{ + client: walletrpc.NewWalletKitClient(conn), + } +} + +func (m *walletKitClient) DeriveNextKey(ctx context.Context, family int32) ( + *keychain.KeyDescriptor, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := m.client.DeriveNextKey(rpcCtx, &walletrpc.KeyReq{ + KeyFamily: family, + }) + if err != nil { + return nil, err + } + + key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256()) + if err != nil { + return nil, err + } + + return &keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(resp.KeyLoc.KeyFamily), + Index: uint32(resp.KeyLoc.KeyIndex), + }, + PubKey: key, + }, nil +} + +func (m *walletKitClient) DeriveKey(ctx context.Context, in *keychain.KeyLocator) ( + *keychain.KeyDescriptor, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := m.client.DeriveKey(rpcCtx, &signrpc.KeyLocator{ + KeyFamily: int32(in.Family), + KeyIndex: int32(in.Index), + }) + if err != nil { + return nil, err + } + + key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256()) + if err != nil { + return nil, err + } + + return &keychain.KeyDescriptor{ + KeyLocator: *in, + PubKey: key, + }, nil +} + +func (m *walletKitClient) NextAddr(ctx context.Context) ( + btcutil.Address, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := m.client.NextAddr(rpcCtx, &walletrpc.AddrRequest{}) + if err != nil { + return nil, err + } + + addr, err := btcutil.DecodeAddress(resp.Addr, nil) + if err != nil { + return nil, err + } + + return addr, nil +} + +func (m *walletKitClient) PublishTransaction(ctx context.Context, + tx *wire.MsgTx) error { + + txHex, err := utils.EncodeTx(tx) + if err != nil { + return err + } + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + _, err = m.client.PublishTransaction(rpcCtx, &walletrpc.Transaction{ + TxHex: txHex, + }) + + return err +} + +func (m *walletKitClient) SendOutputs(ctx context.Context, + outputs []*wire.TxOut, feeRate lnwallet.SatPerKWeight) ( + *wire.MsgTx, error) { + + rpcOutputs := make([]*signrpc.TxOut, len(outputs)) + for i, output := range outputs { + rpcOutputs[i] = &signrpc.TxOut{ + PkScript: output.PkScript, + Value: output.Value, + } + } + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := m.client.SendOutputs(rpcCtx, &walletrpc.SendOutputsRequest{ + Outputs: rpcOutputs, + SatPerKw: int64(feeRate), + }) + if err != nil { + return nil, err + } + + tx, err := utils.DecodeTx(resp.RawTx) + if err != nil { + return nil, err + } + + return tx, nil +} + +func (m *walletKitClient) EstimateFee(ctx context.Context, confTarget int32) ( + lnwallet.SatPerKWeight, error) { + + rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + resp, err := m.client.EstimateFee(rpcCtx, &walletrpc.EstimateFeeRequest{ + ConfTarget: int32(confTarget), + }) + if err != nil { + return 0, err + } + + return lnwallet.SatPerKWeight(resp.SatPerKw), nil +} diff --git a/rpc/gen_protos.sh b/rpc/gen_protos.sh new file mode 100755 index 0000000..ec4e8e9 --- /dev/null +++ b/rpc/gen_protos.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Generate the protos. + protoc -I/usr/local/include -I.\ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --go_out=plugins=grpc,paths=source_relative:. \ + server.proto diff --git a/rpc/server.pb.go b/rpc/server.pb.go new file mode 100644 index 0000000..226b7c1 --- /dev/null +++ b/rpc/server.pb.go @@ -0,0 +1,405 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: server.proto + +package rpc // import "github.com/lightninglabs/nautilus/rpc" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import _ "google.golang.org/genproto/googleapis/api/annotations" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type ServerUnchargeSwapRequest struct { + ReceiverKey []byte `protobuf:"bytes,1,opt,name=receiver_key,json=receiverKey,proto3" json:"receiver_key,omitempty"` + SwapHash []byte `protobuf:"bytes,2,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` + Amt uint64 `protobuf:"varint,3,opt,name=amt,proto3" json:"amt,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerUnchargeSwapRequest) Reset() { *m = ServerUnchargeSwapRequest{} } +func (m *ServerUnchargeSwapRequest) String() string { return proto.CompactTextString(m) } +func (*ServerUnchargeSwapRequest) ProtoMessage() {} +func (*ServerUnchargeSwapRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_server_a93cbdd892155ac8, []int{0} +} +func (m *ServerUnchargeSwapRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerUnchargeSwapRequest.Unmarshal(m, b) +} +func (m *ServerUnchargeSwapRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerUnchargeSwapRequest.Marshal(b, m, deterministic) +} +func (dst *ServerUnchargeSwapRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerUnchargeSwapRequest.Merge(dst, src) +} +func (m *ServerUnchargeSwapRequest) XXX_Size() int { + return xxx_messageInfo_ServerUnchargeSwapRequest.Size(m) +} +func (m *ServerUnchargeSwapRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ServerUnchargeSwapRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerUnchargeSwapRequest proto.InternalMessageInfo + +func (m *ServerUnchargeSwapRequest) GetReceiverKey() []byte { + if m != nil { + return m.ReceiverKey + } + return nil +} + +func (m *ServerUnchargeSwapRequest) GetSwapHash() []byte { + if m != nil { + return m.SwapHash + } + return nil +} + +func (m *ServerUnchargeSwapRequest) GetAmt() uint64 { + if m != nil { + return m.Amt + } + return 0 +} + +type ServerUnchargeSwapResponse struct { + SwapInvoice string `protobuf:"bytes,1,opt,name=swap_invoice,json=swapInvoice,proto3" json:"swap_invoice,omitempty"` + PrepayInvoice string `protobuf:"bytes,2,opt,name=prepay_invoice,json=prepayInvoice,proto3" json:"prepay_invoice,omitempty"` + SenderKey []byte `protobuf:"bytes,3,opt,name=sender_key,json=senderKey,proto3" json:"sender_key,omitempty"` + Expiry int32 `protobuf:"varint,4,opt,name=expiry,proto3" json:"expiry,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerUnchargeSwapResponse) Reset() { *m = ServerUnchargeSwapResponse{} } +func (m *ServerUnchargeSwapResponse) String() string { return proto.CompactTextString(m) } +func (*ServerUnchargeSwapResponse) ProtoMessage() {} +func (*ServerUnchargeSwapResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_server_a93cbdd892155ac8, []int{1} +} +func (m *ServerUnchargeSwapResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerUnchargeSwapResponse.Unmarshal(m, b) +} +func (m *ServerUnchargeSwapResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerUnchargeSwapResponse.Marshal(b, m, deterministic) +} +func (dst *ServerUnchargeSwapResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerUnchargeSwapResponse.Merge(dst, src) +} +func (m *ServerUnchargeSwapResponse) XXX_Size() int { + return xxx_messageInfo_ServerUnchargeSwapResponse.Size(m) +} +func (m *ServerUnchargeSwapResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerUnchargeSwapResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerUnchargeSwapResponse proto.InternalMessageInfo + +func (m *ServerUnchargeSwapResponse) GetSwapInvoice() string { + if m != nil { + return m.SwapInvoice + } + return "" +} + +func (m *ServerUnchargeSwapResponse) GetPrepayInvoice() string { + if m != nil { + return m.PrepayInvoice + } + return "" +} + +func (m *ServerUnchargeSwapResponse) GetSenderKey() []byte { + if m != nil { + return m.SenderKey + } + return nil +} + +func (m *ServerUnchargeSwapResponse) GetExpiry() int32 { + if m != nil { + return m.Expiry + } + return 0 +} + +type ServerUnchargeQuoteRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerUnchargeQuoteRequest) Reset() { *m = ServerUnchargeQuoteRequest{} } +func (m *ServerUnchargeQuoteRequest) String() string { return proto.CompactTextString(m) } +func (*ServerUnchargeQuoteRequest) ProtoMessage() {} +func (*ServerUnchargeQuoteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_server_a93cbdd892155ac8, []int{2} +} +func (m *ServerUnchargeQuoteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerUnchargeQuoteRequest.Unmarshal(m, b) +} +func (m *ServerUnchargeQuoteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerUnchargeQuoteRequest.Marshal(b, m, deterministic) +} +func (dst *ServerUnchargeQuoteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerUnchargeQuoteRequest.Merge(dst, src) +} +func (m *ServerUnchargeQuoteRequest) XXX_Size() int { + return xxx_messageInfo_ServerUnchargeQuoteRequest.Size(m) +} +func (m *ServerUnchargeQuoteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ServerUnchargeQuoteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerUnchargeQuoteRequest proto.InternalMessageInfo + +type ServerUnchargeQuoteResponse struct { + SwapPaymentDest string `protobuf:"bytes,1,opt,name=swap_payment_dest,json=swapPaymentDest,proto3" json:"swap_payment_dest,omitempty"` + SwapFeeBase int64 `protobuf:"varint,2,opt,name=swap_fee_base,json=swapFeeBase,proto3" json:"swap_fee_base,omitempty"` + SwapFeeRate int64 `protobuf:"varint,3,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"` + PrepayAmt uint64 `protobuf:"varint,4,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"` + MinSwapAmount uint64 `protobuf:"varint,5,opt,name=min_swap_amount,json=minSwapAmount,proto3" json:"min_swap_amount,omitempty"` + MaxSwapAmount uint64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"` + CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerUnchargeQuoteResponse) Reset() { *m = ServerUnchargeQuoteResponse{} } +func (m *ServerUnchargeQuoteResponse) String() string { return proto.CompactTextString(m) } +func (*ServerUnchargeQuoteResponse) ProtoMessage() {} +func (*ServerUnchargeQuoteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_server_a93cbdd892155ac8, []int{3} +} +func (m *ServerUnchargeQuoteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerUnchargeQuoteResponse.Unmarshal(m, b) +} +func (m *ServerUnchargeQuoteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerUnchargeQuoteResponse.Marshal(b, m, deterministic) +} +func (dst *ServerUnchargeQuoteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerUnchargeQuoteResponse.Merge(dst, src) +} +func (m *ServerUnchargeQuoteResponse) XXX_Size() int { + return xxx_messageInfo_ServerUnchargeQuoteResponse.Size(m) +} +func (m *ServerUnchargeQuoteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerUnchargeQuoteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerUnchargeQuoteResponse proto.InternalMessageInfo + +func (m *ServerUnchargeQuoteResponse) GetSwapPaymentDest() string { + if m != nil { + return m.SwapPaymentDest + } + return "" +} + +func (m *ServerUnchargeQuoteResponse) GetSwapFeeBase() int64 { + if m != nil { + return m.SwapFeeBase + } + return 0 +} + +func (m *ServerUnchargeQuoteResponse) GetSwapFeeRate() int64 { + if m != nil { + return m.SwapFeeRate + } + return 0 +} + +func (m *ServerUnchargeQuoteResponse) GetPrepayAmt() uint64 { + if m != nil { + return m.PrepayAmt + } + return 0 +} + +func (m *ServerUnchargeQuoteResponse) GetMinSwapAmount() uint64 { + if m != nil { + return m.MinSwapAmount + } + return 0 +} + +func (m *ServerUnchargeQuoteResponse) GetMaxSwapAmount() uint64 { + if m != nil { + return m.MaxSwapAmount + } + return 0 +} + +func (m *ServerUnchargeQuoteResponse) GetCltvDelta() int32 { + if m != nil { + return m.CltvDelta + } + return 0 +} + +func init() { + proto.RegisterType((*ServerUnchargeSwapRequest)(nil), "rpc.ServerUnchargeSwapRequest") + proto.RegisterType((*ServerUnchargeSwapResponse)(nil), "rpc.ServerUnchargeSwapResponse") + proto.RegisterType((*ServerUnchargeQuoteRequest)(nil), "rpc.ServerUnchargeQuoteRequest") + proto.RegisterType((*ServerUnchargeQuoteResponse)(nil), "rpc.ServerUnchargeQuoteResponse") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// SwapServerClient is the client API for SwapServer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type SwapServerClient interface { + NewUnchargeSwap(ctx context.Context, in *ServerUnchargeSwapRequest, opts ...grpc.CallOption) (*ServerUnchargeSwapResponse, error) + UnchargeQuote(ctx context.Context, in *ServerUnchargeQuoteRequest, opts ...grpc.CallOption) (*ServerUnchargeQuoteResponse, error) +} + +type swapServerClient struct { + cc *grpc.ClientConn +} + +func NewSwapServerClient(cc *grpc.ClientConn) SwapServerClient { + return &swapServerClient{cc} +} + +func (c *swapServerClient) NewUnchargeSwap(ctx context.Context, in *ServerUnchargeSwapRequest, opts ...grpc.CallOption) (*ServerUnchargeSwapResponse, error) { + out := new(ServerUnchargeSwapResponse) + err := c.cc.Invoke(ctx, "/rpc.SwapServer/NewUnchargeSwap", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapServerClient) UnchargeQuote(ctx context.Context, in *ServerUnchargeQuoteRequest, opts ...grpc.CallOption) (*ServerUnchargeQuoteResponse, error) { + out := new(ServerUnchargeQuoteResponse) + err := c.cc.Invoke(ctx, "/rpc.SwapServer/UnchargeQuote", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SwapServerServer is the server API for SwapServer service. +type SwapServerServer interface { + NewUnchargeSwap(context.Context, *ServerUnchargeSwapRequest) (*ServerUnchargeSwapResponse, error) + UnchargeQuote(context.Context, *ServerUnchargeQuoteRequest) (*ServerUnchargeQuoteResponse, error) +} + +func RegisterSwapServerServer(s *grpc.Server, srv SwapServerServer) { + s.RegisterService(&_SwapServer_serviceDesc, srv) +} + +func _SwapServer_NewUnchargeSwap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ServerUnchargeSwapRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).NewUnchargeSwap(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rpc.SwapServer/NewUnchargeSwap", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).NewUnchargeSwap(ctx, req.(*ServerUnchargeSwapRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapServer_UnchargeQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ServerUnchargeQuoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).UnchargeQuote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rpc.SwapServer/UnchargeQuote", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).UnchargeQuote(ctx, req.(*ServerUnchargeQuoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _SwapServer_serviceDesc = grpc.ServiceDesc{ + ServiceName: "rpc.SwapServer", + HandlerType: (*SwapServerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "NewUnchargeSwap", + Handler: _SwapServer_NewUnchargeSwap_Handler, + }, + { + MethodName: "UnchargeQuote", + Handler: _SwapServer_UnchargeQuote_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "server.proto", +} + +func init() { proto.RegisterFile("server.proto", fileDescriptor_server_a93cbdd892155ac8) } + +var fileDescriptor_server_a93cbdd892155ac8 = []byte{ + // 471 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x93, 0xc1, 0x8e, 0xd3, 0x30, + 0x10, 0x86, 0x95, 0xb6, 0x5b, 0xe8, 0x6c, 0x4b, 0x21, 0x07, 0x14, 0xba, 0xbb, 0x50, 0x2a, 0x2d, + 0x54, 0x1c, 0x1a, 0x09, 0x9e, 0x60, 0x57, 0x2b, 0x04, 0x42, 0x42, 0x90, 0x15, 0x17, 0x2e, 0xd1, + 0x34, 0x1d, 0x12, 0x8b, 0xc4, 0x36, 0xb6, 0xd3, 0x36, 0x0f, 0x83, 0x78, 0x09, 0x1e, 0x10, 0xd9, + 0xce, 0x42, 0x8b, 0xda, 0x5b, 0xf2, 0xcf, 0xe7, 0x99, 0x7f, 0x7e, 0x27, 0x30, 0xd4, 0xa4, 0xd6, + 0xa4, 0x16, 0x52, 0x09, 0x23, 0xc2, 0xae, 0x92, 0xd9, 0xe4, 0x3c, 0x17, 0x22, 0x2f, 0x29, 0x46, + 0xc9, 0x62, 0xe4, 0x5c, 0x18, 0x34, 0x4c, 0x70, 0xed, 0x91, 0x59, 0x05, 0x4f, 0x6e, 0xdd, 0x91, + 0x2f, 0x3c, 0x2b, 0x50, 0xe5, 0x74, 0xbb, 0x41, 0x99, 0xd0, 0x8f, 0x9a, 0xb4, 0x09, 0x9f, 0xc3, + 0x50, 0x51, 0x46, 0x6c, 0x4d, 0x2a, 0xfd, 0x4e, 0x4d, 0x14, 0x4c, 0x83, 0xf9, 0x30, 0x39, 0xbd, + 0xd3, 0x3e, 0x50, 0x13, 0x9e, 0xc1, 0x40, 0x6f, 0x50, 0xa6, 0x05, 0xea, 0x22, 0xea, 0xb8, 0xfa, + 0x7d, 0x2b, 0xbc, 0x43, 0x5d, 0x84, 0x0f, 0xa1, 0x8b, 0x95, 0x89, 0xba, 0xd3, 0x60, 0xde, 0x4b, + 0xec, 0xe3, 0xec, 0x67, 0x00, 0x93, 0x43, 0xf3, 0xb4, 0x14, 0x5c, 0x93, 0x1d, 0xe8, 0xba, 0x31, + 0xbe, 0x16, 0x2c, 0x23, 0x37, 0x70, 0x90, 0x9c, 0x5a, 0xed, 0xbd, 0x97, 0xc2, 0x4b, 0x78, 0x20, + 0x15, 0x49, 0x6c, 0xfe, 0x42, 0x1d, 0x07, 0x8d, 0xbc, 0x7a, 0x87, 0x5d, 0x00, 0x68, 0xe2, 0xab, + 0xd6, 0x78, 0xd7, 0x19, 0x1b, 0x78, 0xc5, 0xda, 0x7e, 0x0c, 0x7d, 0xda, 0x4a, 0xa6, 0x9a, 0xa8, + 0x37, 0x0d, 0xe6, 0x27, 0x49, 0xfb, 0x36, 0x3b, 0xff, 0xdf, 0xde, 0xe7, 0x5a, 0x18, 0x6a, 0xf3, + 0x98, 0xfd, 0xea, 0xc0, 0xd9, 0xc1, 0x72, 0x6b, 0xff, 0x15, 0x3c, 0x72, 0xf6, 0x25, 0x36, 0x15, + 0x71, 0x93, 0xae, 0x48, 0x9b, 0x76, 0x87, 0xb1, 0x2d, 0x7c, 0xf2, 0xfa, 0x8d, 0xcd, 0x76, 0x06, + 0x23, 0xc7, 0x7e, 0x23, 0x4a, 0x97, 0xa8, 0xfd, 0x1a, 0x5d, 0xbf, 0xeb, 0x5b, 0xa2, 0x6b, 0xd4, + 0xb4, 0xc7, 0x28, 0x34, 0xe4, 0xf6, 0xf8, 0xc7, 0x24, 0x68, 0xdc, 0xa2, 0x6d, 0x1e, 0x36, 0xea, + 0x9e, 0x8b, 0x7a, 0xe0, 0x95, 0xab, 0xca, 0x84, 0x2f, 0x60, 0x5c, 0x31, 0x9e, 0xba, 0x36, 0x58, + 0x89, 0x9a, 0x9b, 0xe8, 0xc4, 0x31, 0xa3, 0x8a, 0x71, 0x9b, 0xfd, 0x95, 0x13, 0x1d, 0x87, 0xdb, + 0x3d, 0xae, 0xdf, 0x72, 0xb8, 0xdd, 0xe1, 0x2e, 0x00, 0xb2, 0xd2, 0xac, 0xd3, 0x15, 0x95, 0x06, + 0xa3, 0x7b, 0x2e, 0xbc, 0x81, 0x55, 0x6e, 0xac, 0xf0, 0xfa, 0x77, 0x00, 0x60, 0x69, 0x9f, 0x52, + 0x98, 0xc0, 0xf8, 0x23, 0x6d, 0x76, 0xaf, 0x3a, 0x7c, 0xba, 0x50, 0x32, 0x5b, 0x1c, 0xfd, 0xe6, + 0x26, 0xcf, 0x8e, 0xd6, 0xdb, 0x90, 0x13, 0x18, 0xed, 0xa5, 0x1f, 0x1e, 0x3a, 0xb1, 0x7b, 0x6d, + 0x93, 0xe9, 0x71, 0xc0, 0xf7, 0xbc, 0x7e, 0xf9, 0xf5, 0x32, 0x67, 0xa6, 0xa8, 0x97, 0x8b, 0x4c, + 0x54, 0x71, 0xc9, 0xf2, 0xc2, 0x70, 0xc6, 0xf3, 0x12, 0x97, 0x3a, 0xe6, 0x58, 0x1b, 0x56, 0xd6, + 0x3a, 0x56, 0x32, 0x5b, 0xf6, 0xdd, 0x5f, 0xf3, 0xe6, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xeb, + 0xb5, 0x41, 0x9b, 0x68, 0x03, 0x00, 0x00, +} diff --git a/rpc/server.proto b/rpc/server.proto new file mode 100644 index 0000000..e97a6a8 --- /dev/null +++ b/rpc/server.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; + +package rpc; + +option go_package = "github.com/lightninglabs/nautilus/rpc"; + +service SwapServer { + rpc NewUnchargeSwap(ServerUnchargeSwapRequest) returns (ServerUnchargeSwapResponse); + + rpc UnchargeQuote(ServerUnchargeQuoteRequest) returns (ServerUnchargeQuoteResponse); +} + +message ServerUnchargeSwapRequest { + bytes receiver_key = 1; + bytes swap_hash = 2; + uint64 amt = 3; + +} + +message ServerUnchargeSwapResponse { + string swap_invoice= 1; + string prepay_invoice = 2; + bytes sender_key = 3; + int32 expiry = 4; +} + +message ServerUnchargeQuoteRequest { +} + +message ServerUnchargeQuoteResponse { + string swap_payment_dest = 1; + int64 swap_fee_base = 2; + int64 swap_fee_rate = 3; + uint64 prepay_amt = 4; + uint64 min_swap_amount = 5; + uint64 max_swap_amount = 6; + int32 cltv_delta = 7; +} diff --git a/sweep/sweeper.go b/sweep/sweeper.go new file mode 100644 index 0000000..eeabdd6 --- /dev/null +++ b/sweep/sweeper.go @@ -0,0 +1,106 @@ +package sweep + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +// Sweeper creates htlc sweep txes. +type Sweeper struct { + Lnd *lndclient.LndServices +} + +// CreateSweepTx creates an htlc sweep tx. +func (s *Sweeper) CreateSweepTx( + globalCtx context.Context, height int32, + htlc *utils.Htlc, htlcOutpoint wire.OutPoint, + keyBytes [33]byte, + witnessFunc func(sig []byte) (wire.TxWitness, error), + amount, fee btcutil.Amount, + destAddr btcutil.Address) (*wire.MsgTx, error) { + + // Compose tx. + sweepTx := wire.NewMsgTx(2) + + sweepTx.LockTime = uint32(height) + + // Add HTLC input. + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: htlcOutpoint, + }) + + // Add output for the destination address. + sweepPkScript, err := txscript.PayToAddrScript(destAddr) + if err != nil { + return nil, err + } + + sweepTx.AddTxOut(&wire.TxOut{ + PkScript: sweepPkScript, + Value: int64(amount - fee), + }) + + // Generate a signature for the swap htlc transaction. + + key, err := btcec.ParsePubKey(keyBytes[:], btcec.S256()) + if err != nil { + return nil, err + } + + signDesc := input.SignDescriptor{ + WitnessScript: htlc.Script, + Output: &wire.TxOut{ + Value: int64(amount), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + KeyDesc: keychain.KeyDescriptor{ + PubKey: key, + }, + } + + rawSigs, err := s.Lnd.Signer.SignOutputRaw( + globalCtx, sweepTx, []*input.SignDescriptor{&signDesc}, + ) + if err != nil { + return nil, fmt.Errorf("signing: %v", err) + } + sig := rawSigs[0] + + // Add witness stack to the tx input. + sweepTx.TxIn[0].Witness, err = witnessFunc(sig) + if err != nil { + return nil, err + } + + return sweepTx, nil +} + +// GetSweepFee calculates the required tx fee. +func (s *Sweeper) GetSweepFee(ctx context.Context, + htlcSuccessWitnessSize int, sweepConfTarget int32) ( + btcutil.Amount, error) { + + // Get fee estimate from lnd. + feeRate, err := s.Lnd.WalletKit.EstimateFee(ctx, sweepConfTarget) + if err != nil { + return 0, fmt.Errorf("estimate fee: %v", err) + } + + // Calculate weight for this tx. + var weightEstimate input.TxWeightEstimator + weightEstimate.AddP2WKHOutput() + weightEstimate.AddWitnessInput(htlcSuccessWitnessSize) + weight := weightEstimate.Weight() + + return feeRate.FeeForWeight(int64(weight)), nil +} diff --git a/test/chainnotifier_mock.go b/test/chainnotifier_mock.go new file mode 100644 index 0000000..375de38 --- /dev/null +++ b/test/chainnotifier_mock.go @@ -0,0 +1,134 @@ +package test + +import ( + "sync" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/chainntnfs" + "golang.org/x/net/context" +) + +type mockChainNotifier struct { + lnd *LndMockServices + wg sync.WaitGroup +} + +// SpendRegistration contains registration details. +type SpendRegistration struct { + Outpoint *wire.OutPoint + PkScript []byte + HeightHint int32 +} + +// ConfRegistration contains registration details. +type ConfRegistration struct { + TxID *chainhash.Hash + PkScript []byte + HeightHint int32 + NumConfs int32 +} + +func (c *mockChainNotifier) RegisterSpendNtfn(ctx context.Context, + outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( + chan *chainntnfs.SpendDetail, chan error, error) { + + c.lnd.RegisterSpendChannel <- &SpendRegistration{ + HeightHint: heightHint, + Outpoint: outpoint, + PkScript: pkScript, + } + + spendChan := make(chan *chainntnfs.SpendDetail, 1) + errChan := make(chan error, 1) + + c.wg.Add(1) + go func() { + defer c.wg.Done() + + select { + case m := <-c.lnd.SpendChannel: + select { + case spendChan <- m: + case <-ctx.Done(): + } + case <-ctx.Done(): + } + }() + + return spendChan, errChan, nil +} + +func (c *mockChainNotifier) WaitForFinished() { + c.wg.Wait() +} + +func (c *mockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) ( + chan int32, chan error, error) { + + blockErrorChan := make(chan error, 1) + blockEpochChan := make(chan int32) + + c.wg.Add(1) + go func() { + defer c.wg.Done() + + // Send initial block height + select { + case blockEpochChan <- c.lnd.Height: + case <-ctx.Done(): + return + } + + for { + select { + case m := <-c.lnd.epochChannel: + select { + case blockEpochChan <- m: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } + }() + + return blockEpochChan, blockErrorChan, nil +} + +func (c *mockChainNotifier) RegisterConfirmationsNtfn(ctx context.Context, + txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) ( + chan *chainntnfs.TxConfirmation, chan error, error) { + + confChan := make(chan *chainntnfs.TxConfirmation, 1) + errChan := make(chan error, 1) + + c.wg.Add(1) + go func() { + defer c.wg.Done() + + select { + case m := <-c.lnd.ConfChannel: + select { + case confChan <- m: + case <-ctx.Done(): + } + case <-ctx.Done(): + } + }() + + select { + case c.lnd.RegisterConfChannel <- &ConfRegistration{ + PkScript: pkScript, + TxID: txid, + HeightHint: heightHint, + NumConfs: numConfs, + }: + case <-time.After(Timeout): + return nil, nil, ErrTimeout + } + + return confChan, errChan, nil +} diff --git a/test/context.go b/test/context.go new file mode 100644 index 0000000..f11ff1d --- /dev/null +++ b/test/context.go @@ -0,0 +1,240 @@ +package test + +import ( + "bytes" + "crypto/sha256" + "testing" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/zpay32" +) + +// Context contains shared test context functions. +type Context struct { + T *testing.T + Lnd *LndMockServices + FailedInvoices map[lntypes.Hash]struct{} + PaidInvoices map[string]func(error) +} + +// NewContext instanties a new common test context. +func NewContext(t *testing.T, + lnd *LndMockServices) Context { + + return Context{ + T: t, + Lnd: lnd, + FailedInvoices: make(map[lntypes.Hash]struct{}), + PaidInvoices: make(map[string]func(error)), + } +} + +// ReceiveTx receives and decodes a published tx. +func (ctx *Context) ReceiveTx() *wire.MsgTx { + ctx.T.Helper() + + select { + case tx := <-ctx.Lnd.TxPublishChannel: + return tx + case <-time.After(Timeout): + ctx.T.Fatalf("sweep not published") + return nil + } +} + +// NotifySpend simulates a spend. +func (ctx *Context) NotifySpend(tx *wire.MsgTx, inputIndex uint32) { + ctx.T.Helper() + + txHash := tx.TxHash() + + select { + case ctx.Lnd.SpendChannel <- &chainntnfs.SpendDetail{ + SpendingTx: tx, + SpenderTxHash: &txHash, + SpenderInputIndex: inputIndex, + }: + case <-time.After(Timeout): + ctx.T.Fatalf("htlc spend not consumed") + + } +} + +// NotifyConf simulates a conf. +func (ctx *Context) NotifyConf(tx *wire.MsgTx) { + ctx.T.Helper() + + select { + case ctx.Lnd.ConfChannel <- &chainntnfs.TxConfirmation{ + Tx: tx, + }: + case <-time.After(Timeout): + ctx.T.Fatalf("htlc spend not consumed") + + } +} + +// AssertRegisterSpendNtfn asserts that a register for spend has been received. +func (ctx *Context) AssertRegisterSpendNtfn(script []byte) { + ctx.T.Helper() + + select { + case spendIntent := <-ctx.Lnd.RegisterSpendChannel: + if !bytes.Equal(spendIntent.PkScript, script) { + ctx.T.Fatalf("server not listening for published htlc script") + } + case <-time.After(Timeout): + DumpGoroutines() + ctx.T.Fatalf("spend not subscribed to") + } +} + +// AssertRegisterConf asserts that a register for conf has been received. +func (ctx *Context) AssertRegisterConf() *ConfRegistration { + ctx.T.Helper() + + // Expect client to register for conf + var confIntent *ConfRegistration + select { + case confIntent = <-ctx.Lnd.RegisterConfChannel: + if confIntent.TxID != nil { + ctx.T.Fatalf("expected script only registration") + } + case <-time.After(Timeout): + ctx.T.Fatalf("htlc confirmed not subscribed to") + } + + return confIntent +} + +// AssertPaid asserts that the expected payment request has been paid. This +// function returns a complete function to signal the final payment result. +func (ctx *Context) AssertPaid( + expectedMemo string) func(error) { + + ctx.T.Helper() + + if done, ok := ctx.PaidInvoices[expectedMemo]; ok { + return done + } + + // Assert that client pays swap invoice. + for { + var swapPayment PaymentChannelMessage + select { + case swapPayment = <-ctx.Lnd.SendPaymentChannel: + case <-time.After(Timeout): + ctx.T.Fatalf("no payment sent for invoice: %v", + expectedMemo) + } + + payReq := ctx.DecodeInvoice(swapPayment.PaymentRequest) + + if _, ok := ctx.PaidInvoices[*payReq.Description]; ok { + ctx.T.Fatalf("duplicate invoice paid: %v", + *payReq.Description) + } + + done := func(result error) { + select { + case swapPayment.Done <- result: + case <-time.After(Timeout): + ctx.T.Fatalf("payment result not consumed") + } + } + + ctx.PaidInvoices[*payReq.Description] = done + + if *payReq.Description == expectedMemo { + return done + } + } +} + +// AssertSettled asserts that an invoice with the given hash is settled. +func (ctx *Context) AssertSettled( + expectedHash lntypes.Hash) lntypes.Preimage { + + ctx.T.Helper() + + select { + case preimage := <-ctx.Lnd.SettleInvoiceChannel: + hash := sha256.Sum256(preimage[:]) + if expectedHash != hash { + ctx.T.Fatalf("server claims with wrong preimage") + } + + return preimage + case <-time.After(Timeout): + } + ctx.T.Fatalf("invoice not settled") + return lntypes.Preimage{} +} + +// AssertFailed asserts that an invoice with the given hash is failed. +func (ctx *Context) AssertFailed(expectedHash lntypes.Hash) { + ctx.T.Helper() + + if _, ok := ctx.FailedInvoices[expectedHash]; ok { + return + } + + for { + select { + case hash := <-ctx.Lnd.FailInvoiceChannel: + ctx.FailedInvoices[expectedHash] = struct{}{} + if expectedHash == hash { + return + } + case <-time.After(Timeout): + ctx.T.Fatalf("invoice not failed") + } + } +} + +// DecodeInvoice decodes a payment request string. +func (ctx *Context) DecodeInvoice(request string) *zpay32.Invoice { + ctx.T.Helper() + + payReq, err := ctx.Lnd.DecodeInvoice(request) + if err != nil { + ctx.T.Fatal(err) + } + return payReq +} + +func (ctx *Context) GetOutputIndex(tx *wire.MsgTx, + script []byte) int { + + for idx, out := range tx.TxOut { + if bytes.Equal(out.PkScript, script) { + return idx + } + } + + ctx.T.Fatal("htlc not present in tx") + return 0 +} + +// NotifyServerHeight notifies the server of the arrival of a new block and +// waits for the notification to be processed by selecting on a +// dedicated test channel. +func (ctx *Context) NotifyServerHeight(height int32) { + if err := ctx.Lnd.NotifyHeight(height); err != nil { + ctx.T.Fatal(err) + } + + // TODO: Fix race condition with height not processed yet. + + // select { + // case h := <-ctx.swapServer.testEpochChan: + // if h != height { + // ctx.T.Fatal("height not set") + // } + // case <-time.After(test.Timeout): + // ctx.T.Fatal("no height response") + // } +} diff --git a/test/invoices_mock.go b/test/invoices_mock.go new file mode 100644 index 0000000..1b6133f --- /dev/null +++ b/test/invoices_mock.go @@ -0,0 +1,111 @@ +package test + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" +) + +type mockInvoices struct { + lnd *LndMockServices + wg sync.WaitGroup +} + +func (s *mockInvoices) SettleInvoice(ctx context.Context, + preimage lntypes.Preimage) error { + + logger.Infof("Settle invoice %v with preimage %v", preimage.Hash(), + preimage) + + s.lnd.SettleInvoiceChannel <- preimage + + return nil +} + +func (s *mockInvoices) WaitForFinished() { + s.wg.Wait() +} + +func (s *mockInvoices) CancelInvoice(ctx context.Context, + hash lntypes.Hash) error { + + s.lnd.FailInvoiceChannel <- hash + + return nil +} + +func (s *mockInvoices) SubscribeSingleInvoice(ctx context.Context, + hash lntypes.Hash) (<-chan channeldb.ContractState, + <-chan error, error) { + + updateChan := make(chan channeldb.ContractState, 2) + errChan := make(chan error) + + select { + case s.lnd.SingleInvoiceSubcribeChannel <- &SingleInvoiceSubscription{ + Update: updateChan, + Err: errChan, + Hash: hash, + }: + case <-ctx.Done(): + return nil, nil, ctx.Err() + } + + return updateChan, errChan, nil +} + +func (s *mockInvoices) AddHoldInvoice(ctx context.Context, + in *invoicesrpc.AddInvoiceData) (string, error) { + + s.lnd.lock.Lock() + defer s.lnd.lock.Unlock() + + hash := in.Hash + + // Create and encode the payment request as a bech32 (zpay32) string. + creationDate := time.Now() + + payReq, err := zpay32.NewInvoice( + s.lnd.ChainParams, *hash, creationDate, + zpay32.Description(in.Memo), + zpay32.CLTVExpiry(in.CltvExpiry), + zpay32.Amount(lnwire.MilliSatoshi(in.Value)), + ) + if err != nil { + return "", err + } + + privKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return "", err + } + + payReqString, err := payReq.Encode( + zpay32.MessageSigner{ + SignCompact: func(hash []byte) ([]byte, error) { + // btcec.SignCompact returns a pubkey-recoverable signature + sig, err := btcec.SignCompact( + btcec.S256(), privKey, hash, true, + ) + if err != nil { + return nil, fmt.Errorf("can't sign the hash: %v", err) + } + + return sig, nil + }, + }, + ) + if err != nil { + return "", err + } + + return payReqString, nil +} diff --git a/test/keys.go b/test/keys.go new file mode 100644 index 0000000..aa11cb2 --- /dev/null +++ b/test/keys.go @@ -0,0 +1,17 @@ +package test + +import ( + "github.com/btcsuite/btcd/btcec" +) + +// CreateKey returns a deterministically generated key pair. +func CreateKey(index int32) (*btcec.PrivateKey, *btcec.PublicKey) { + // Avoid all zeros, because it results in an invalid key. + privKey, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), + []byte{0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, byte(index + 1)}) + + return privKey, pubKey +} diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go new file mode 100644 index 0000000..16efb2e --- /dev/null +++ b/test/lightning_client_mock.go @@ -0,0 +1,152 @@ +package test + +import ( + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightninglabs/nautilus/utils" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" + "golang.org/x/net/context" +) + +type mockLightningClient struct { + lnd *LndMockServices + wg sync.WaitGroup +} + +// PayInvoice pays an invoice. +func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string, + maxFee btcutil.Amount, + outgoingChannel *uint64) chan lndclient.PaymentResult { + + done := make(chan lndclient.PaymentResult, 1) + + mockChan := make(chan error) + h.wg.Add(1) + go func() { + defer h.wg.Done() + + amt, err := utils.GetInvoiceAmt(&chaincfg.TestNet3Params, invoice) + if err != nil { + select { + case done <- lndclient.PaymentResult{ + Err: err, + }: + case <-ctx.Done(): + } + return + } + + var paidFee btcutil.Amount + + err = <-mockChan + if err != nil { + amt = 0 + } else { + paidFee = 1 + } + + select { + case done <- lndclient.PaymentResult{ + Err: err, + PaidFee: paidFee, + PaidAmt: amt, + }: + case <-ctx.Done(): + } + }() + + h.lnd.SendPaymentChannel <- PaymentChannelMessage{ + PaymentRequest: invoice, + Done: mockChan, + } + + return done +} + +func (h *mockLightningClient) WaitForFinished() { + h.wg.Wait() +} + +func (h *mockLightningClient) ConfirmedWalletBalance(ctx context.Context) ( + btcutil.Amount, error) { + + return 1000000, nil +} + +func (h *mockLightningClient) GetInfo(ctx context.Context) (*lndclient.Info, + error) { + + var pubKey [33]byte + return &lndclient.Info{ + BlockHeight: 600, + IdentityPubkey: pubKey, + }, nil +} + +func (h *mockLightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) ( + lnwire.MilliSatoshi, error) { + + return 0, nil +} + +func (h *mockLightningClient) AddInvoice(ctx context.Context, + in *invoicesrpc.AddInvoiceData) (lntypes.Hash, string, error) { + + h.lnd.lock.Lock() + defer h.lnd.lock.Unlock() + + var hash lntypes.Hash + if in.Hash != nil { + hash = *in.Hash + } else { + hash = (*in.Preimage).Hash() + } + + // Create and encode the payment request as a bech32 (zpay32) string. + creationDate := time.Now() + + payReq, err := zpay32.NewInvoice( + h.lnd.ChainParams, hash, creationDate, + zpay32.Description(in.Memo), + zpay32.CLTVExpiry(in.CltvExpiry), + zpay32.Amount(lnwire.MilliSatoshi(in.Value)), + ) + if err != nil { + return lntypes.Hash{}, "", err + } + + privKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return lntypes.Hash{}, "", err + } + + payReqString, err := payReq.Encode( + zpay32.MessageSigner{ + SignCompact: func(hash []byte) ([]byte, error) { + // btcec.SignCompact returns a pubkey-recoverable signature + sig, err := btcec.SignCompact( + btcec.S256(), privKey, hash, true, + ) + if err != nil { + return nil, fmt.Errorf("can't sign the hash: %v", err) + } + + return sig, nil + }, + }, + ) + if err != nil { + return lntypes.Hash{}, "", err + } + + return hash, payReqString, nil +} diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go new file mode 100644 index 0000000..2e11338 --- /dev/null +++ b/test/lnd_services_mock.go @@ -0,0 +1,176 @@ +package test + +import ( + "errors" + "sync" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/zpay32" + + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/nautilus/lndclient" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" +) + +var testStartingHeight = int32(600) + +// NewMockLnd returns a new instance of LndMockServices that can be used in unit +// tests. +func NewMockLnd() *LndMockServices { + lightningClient := &mockLightningClient{} + walletKit := &mockWalletKit{} + chainNotifier := &mockChainNotifier{} + signer := &mockSigner{} + invoices := &mockInvoices{} + + lnd := LndMockServices{ + LndServices: lndclient.LndServices{ + WalletKit: walletKit, + Client: lightningClient, + ChainNotifier: chainNotifier, + Signer: signer, + Invoices: invoices, + ChainParams: &chaincfg.TestNet3Params, + }, + SendPaymentChannel: make(chan PaymentChannelMessage), + ConfChannel: make(chan *chainntnfs.TxConfirmation), + RegisterConfChannel: make(chan *ConfRegistration), + RegisterSpendChannel: make(chan *SpendRegistration), + SpendChannel: make(chan *chainntnfs.SpendDetail), + TxPublishChannel: make(chan *wire.MsgTx), + SendOutputsChannel: make(chan wire.MsgTx), + SettleInvoiceChannel: make(chan lntypes.Preimage), + SingleInvoiceSubcribeChannel: make(chan *SingleInvoiceSubscription), + + FailInvoiceChannel: make(chan lntypes.Hash, 2), + epochChannel: make(chan int32), + Height: testStartingHeight, + } + + lightningClient.lnd = &lnd + chainNotifier.lnd = &lnd + walletKit.lnd = &lnd + invoices.lnd = &lnd + + lnd.WaitForFinished = func() { + chainNotifier.WaitForFinished() + lightningClient.WaitForFinished() + invoices.WaitForFinished() + } + + return &lnd +} + +// PaymentChannelMessage is the data that passed through SendPaymentChannel. +type PaymentChannelMessage struct { + PaymentRequest string + Done chan error +} + +// SingleInvoiceSubscription contains the single invoice subscribers +type SingleInvoiceSubscription struct { + Hash lntypes.Hash + Update chan channeldb.ContractState + Err chan error +} + +// LndMockServices provides a full set of mocked lnd services. +type LndMockServices struct { + lndclient.LndServices + + SendPaymentChannel chan PaymentChannelMessage + SpendChannel chan *chainntnfs.SpendDetail + TxPublishChannel chan *wire.MsgTx + SendOutputsChannel chan wire.MsgTx + SettleInvoiceChannel chan lntypes.Preimage + FailInvoiceChannel chan lntypes.Hash + epochChannel chan int32 + + ConfChannel chan *chainntnfs.TxConfirmation + RegisterConfChannel chan *ConfRegistration + RegisterSpendChannel chan *SpendRegistration + + SingleInvoiceSubcribeChannel chan *SingleInvoiceSubscription + + Height int32 + + WaitForFinished func() + + lock sync.Mutex +} + +// NotifyHeight notifies a new block height. +func (s *LndMockServices) NotifyHeight(height int32) error { + s.Height = height + + select { + case s.epochChannel <- height: + case <-time.After(Timeout): + return ErrTimeout + } + return nil +} + +// IsDone checks whether all channels have been fully emptied. If not this may +// indicate unexpected behaviour of the code under test. +func (s *LndMockServices) IsDone() error { + select { + case <-s.SendPaymentChannel: + return errors.New("SendPaymentChannel not empty") + default: + } + + select { + case <-s.SpendChannel: + return errors.New("SpendChannel not empty") + default: + } + + select { + case <-s.TxPublishChannel: + return errors.New("TxPublishChannel not empty") + default: + } + + select { + case <-s.SendOutputsChannel: + return errors.New("SendOutputsChannel not empty") + default: + } + + select { + case <-s.SettleInvoiceChannel: + return errors.New("SettleInvoiceChannel not empty") + default: + } + + select { + case <-s.ConfChannel: + return errors.New("ConfChannel not empty") + default: + } + + select { + case <-s.RegisterConfChannel: + return errors.New("RegisterConfChannel not empty") + default: + } + + select { + case <-s.RegisterSpendChannel: + return errors.New("RegisterSpendChannel not empty") + default: + } + + return nil +} + +// DecodeInvoice decodes a payment request string. +func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice, + error) { + + return zpay32.Decode(request, s.ChainParams) +} diff --git a/test/log.go b/test/log.go new file mode 100644 index 0000000..37fa69b --- /dev/null +++ b/test/log.go @@ -0,0 +1,23 @@ +package test + +import ( + "github.com/btcsuite/btclog" + "os" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var ( + backendLog = btclog.NewBackend(logWriter{}) + logger = backendLog.Logger("TEST") +) + +// logWriter implements an io.Writer that outputs to both standard output and +// the write-end pipe of an initialized log rotator. +type logWriter struct{} + +func (logWriter) Write(p []byte) (n int, err error) { + os.Stdout.Write(p) + return len(p), nil +} diff --git a/test/signer_mock.go b/test/signer_mock.go new file mode 100644 index 0000000..8bc86f9 --- /dev/null +++ b/test/signer_mock.go @@ -0,0 +1,19 @@ +package test + +import ( + "context" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/input" +) + +type mockSigner struct { +} + +func (s *mockSigner) SignOutputRaw(ctx context.Context, tx *wire.MsgTx, + signDescriptors []*input.SignDescriptor) ([][]byte, error) { + + rawSigs := [][]byte{{1, 2, 3}} + + return rawSigs, nil +} diff --git a/test/testutils.go b/test/testutils.go new file mode 100644 index 0000000..3ed8941 --- /dev/null +++ b/test/testutils.go @@ -0,0 +1,69 @@ +package test + +import ( + "errors" + "fmt" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/zpay32" + "os" + "runtime/pprof" + "testing" + "time" +) + +var ( + // Timeout is the default timeout when tests wait for something to + // happen. + Timeout = time.Second * 5 + + // ErrTimeout is returned on timeout. + ErrTimeout = errors.New("test timeout") +) + +// GetDestAddr deterministically generates a sweep address for testing. +func GetDestAddr(t *testing.T, nr byte) btcutil.Address { + destAddr, err := btcutil.NewAddressScriptHash([]byte{nr}, + &chaincfg.MainNetParams) + if err != nil { + t.Fatal(err) + } + + return destAddr +} + +// EncodePayReq encodes a zpay32 invoice with a fixed key. +func EncodePayReq(payReq *zpay32.Invoice) (string, error) { + privKey, _ := CreateKey(5) + reqString, err := payReq.Encode( + zpay32.MessageSigner{ + SignCompact: func(hash []byte) ([]byte, error) { + // btcec.SignCompact returns a + // pubkey-recoverable signature + sig, err := btcec.SignCompact( + btcec.S256(), + privKey, + payReq.PaymentHash[:], + true, + ) + if err != nil { + return nil, fmt.Errorf( + "can't sign the hash: %v", err) + } + + return sig, nil + }, + }, + ) + if err != nil { + return "", err + } + + return reqString, nil +} + +// DumpGoroutines dumps all currently running goroutines. +func DumpGoroutines() { + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) +} diff --git a/test/timeout.go b/test/timeout.go new file mode 100644 index 0000000..1f6a8ce --- /dev/null +++ b/test/timeout.go @@ -0,0 +1,31 @@ +package test + +import ( + "os" + "runtime/pprof" + "testing" + "time" + + "github.com/fortytw2/leaktest" +) + +// Guard implements a test level timeout. +func Guard(t *testing.T) func() { + done := make(chan struct{}) + go func() { + select { + case <-time.After(5 * time.Second): + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + + panic("test timeout") + case <-done: + } + }() + + fn := leaktest.Check(t) + + return func() { + close(done) + fn() + } +} diff --git a/test/walletkit_mock.go b/test/walletkit_mock.go new file mode 100644 index 0000000..c8dc5bb --- /dev/null +++ b/test/walletkit_mock.go @@ -0,0 +1,95 @@ +package test + +import ( + "context" + "errors" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwallet" +) + +type mockWalletKit struct { + lnd *LndMockServices + keyIndex int32 +} + +func (m *mockWalletKit) DeriveNextKey(ctx context.Context, family int32) ( + *keychain.KeyDescriptor, error) { + + index := m.keyIndex + + _, pubKey := CreateKey(index) + m.keyIndex++ + + return &keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(family), + Index: uint32(index), + }, + PubKey: pubKey, + }, nil +} + +func (m *mockWalletKit) DeriveKey(ctx context.Context, in *keychain.KeyLocator) ( + *keychain.KeyDescriptor, error) { + + _, pubKey := CreateKey(int32(in.Index)) + + return &keychain.KeyDescriptor{ + KeyLocator: *in, + PubKey: pubKey, + }, nil +} + +func (m *mockWalletKit) NextAddr(ctx context.Context) (btcutil.Address, error) { + addr, err := btcutil.NewAddressWitnessPubKeyHash( + make([]byte, 20), &chaincfg.TestNet3Params, + ) + if err != nil { + return nil, err + } + return addr, nil +} + +func (m *mockWalletKit) PublishTransaction(ctx context.Context, tx *wire.MsgTx) error { + m.lnd.TxPublishChannel <- tx + return nil +} + +func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut, + feeRate lnwallet.SatPerKWeight) (*wire.MsgTx, error) { + + var inputTxHash chainhash.Hash + + tx := wire.MsgTx{} + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: inputTxHash, + Index: 0, + }, + }) + + for _, out := range outputs { + tx.AddTxOut(&wire.TxOut{ + PkScript: out.PkScript, + Value: out.Value, + }) + } + + m.lnd.SendOutputsChannel <- tx + + return &tx, nil +} + +func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) ( + lnwallet.SatPerKWeight, error) { + if confTarget <= 1 { + return 0, errors.New("conf target must be greater than 1") + } + + return 10000, nil +} diff --git a/utils/config.go b/utils/config.go new file mode 100644 index 0000000..3c1024d --- /dev/null +++ b/utils/config.go @@ -0,0 +1,9 @@ +package utils + +// SwapKeyFamily is the key family used to generate keys that allow spending +// of the htlc. +// +// TODO: Decide on actual value. +var ( + SwapKeyFamily = int32(99) +) diff --git a/utils/htlc.go b/utils/htlc.go new file mode 100644 index 0000000..71bc033 --- /dev/null +++ b/utils/htlc.go @@ -0,0 +1,180 @@ +package utils + +import ( + "bytes" + "errors" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" +) + +// Htlc contains relevant htlc information from the receiver perspective. +type Htlc struct { + Script []byte + ScriptHash []byte + Hash lntypes.Hash + MaxSuccessWitnessSize int + MaxTimeoutWitnessSize int +} + +var ( + quoteKey [33]byte + + quoteHash lntypes.Hash + + // QuoteHtlc is a template script just used for fee estimation. It uses + // the maximum value for cltv expiry to get the maximum (worst case) + // script size. + QuoteHtlc, _ = NewHtlc( + ^int32(0), quoteKey, quoteKey, quoteHash, + ) +) + +// NewHtlc returns a new instance. +func NewHtlc(cltvExpiry int32, senderKey, receiverKey [33]byte, + hash lntypes.Hash) (*Htlc, error) { + + script, err := swapHTLCScript( + cltvExpiry, senderKey, receiverKey, hash, + ) + if err != nil { + return nil, err + } + + scriptHash, err := input.WitnessScriptHash(script) + if err != nil { + return nil, err + } + + // Calculate maximum success witness size + // + // - number_of_witness_elements: 1 byte + // - receiver_sig_length: 1 byte + // - receiver_sig: 73 bytes + // - preimage_length: 1 byte + // - preimage: 33 bytes + // - witness_script_length: 1 byte + // - witness_script: len(script) bytes + maxSuccessWitnessSize := 1 + 1 + 73 + 1 + 33 + 1 + len(script) + + // Calculate maximum timeout witness size + // + // - number_of_witness_elements: 1 byte + // - sender_sig_length: 1 byte + // - sender_sig: 73 bytes + // - zero_length: 1 byte + // - zero: 1 byte + // - witness_script_length: 1 byte + // - witness_script: len(script) bytes + maxTimeoutWitnessSize := 1 + 1 + 73 + 1 + 1 + 1 + len(script) + + return &Htlc{ + Hash: hash, + Script: script, + ScriptHash: scriptHash, + MaxSuccessWitnessSize: maxSuccessWitnessSize, + MaxTimeoutWitnessSize: maxTimeoutWitnessSize, + }, nil +} + +// SwapHTLCScript returns the on-chain HTLC witness script. +// +// OP_SIZE 32 OP_EQUAL +// OP_IF +// OP_HASH160 OP_EQUALVERIFY +// +// OP_ELSE +// OP_DROP +// OP_CHECKLOCKTIMEVERIFY OP_DROP +// +// OP_ENDIF +// OP_CHECKSIG +func swapHTLCScript(cltvExpiry int32, senderHtlcKey, + receiverHtlcKey [33]byte, swapHash lntypes.Hash) ([]byte, error) { + + builder := txscript.NewScriptBuilder() + + builder.AddOp(txscript.OP_SIZE) + builder.AddInt64(32) + builder.AddOp(txscript.OP_EQUAL) + + builder.AddOp(txscript.OP_IF) + + builder.AddOp(txscript.OP_HASH160) + builder.AddData(input.Ripemd160H(swapHash[:])) + builder.AddOp(txscript.OP_EQUALVERIFY) + + builder.AddData(receiverHtlcKey[:]) + + builder.AddOp(txscript.OP_ELSE) + + builder.AddOp(txscript.OP_DROP) + + builder.AddInt64(int64(cltvExpiry)) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + builder.AddOp(txscript.OP_DROP) + + builder.AddData(senderHtlcKey[:]) + + builder.AddOp(txscript.OP_ENDIF) + + builder.AddOp(txscript.OP_CHECKSIG) + + return builder.Script() +} + +// Address returns the p2wsh address of the htlc. +func (h *Htlc) Address(chainParams *chaincfg.Params) ( + btcutil.Address, error) { + + // Skip OP_0 and data length. + return btcutil.NewAddressWitnessScriptHash( + h.ScriptHash[2:], + chainParams, + ) +} + +// GenSuccessWitness returns the success script to spend this htlc with the +// preimage. +func (h *Htlc) GenSuccessWitness(receiverSig []byte, + preimage lntypes.Preimage) (wire.TxWitness, error) { + + if h.Hash != preimage.Hash() { + return nil, errors.New("preimage doesn't match hash") + } + + witnessStack := make(wire.TxWitness, 3) + witnessStack[0] = append(receiverSig, byte(txscript.SigHashAll)) + witnessStack[1] = preimage[:] + witnessStack[2] = h.Script + + return witnessStack, nil +} + +// IsSuccessWitness checks whether the given stack is valid for redeeming the +// htlc. +func (h *Htlc) IsSuccessWitness(witness wire.TxWitness) bool { + if len(witness) != 3 { + return false + } + + isTimeoutTx := bytes.Equal([]byte{0}, witness[1]) + + return !isTimeoutTx +} + +// GenTimeoutWitness returns the timeout script to spend this htlc after +// timeout. +func (h *Htlc) GenTimeoutWitness(senderSig []byte) (wire.TxWitness, error) { + + witnessStack := make(wire.TxWitness, 3) + witnessStack[0] = append(senderSig, byte(txscript.SigHashAll)) + witnessStack[1] = []byte{0} + witnessStack[2] = h.Script + + return witnessStack, nil +} diff --git a/utils/swaplog.go b/utils/swaplog.go new file mode 100644 index 0000000..a746389 --- /dev/null +++ b/utils/swaplog.go @@ -0,0 +1,41 @@ +package utils + +import ( + "fmt" + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/lntypes" +) + +// SwapLog logs with a short swap hash prefix. +type SwapLog struct { + Logger btclog.Logger + Hash lntypes.Hash +} + +// Infof formats message according to format specifier and writes to +// log with LevelInfo. +func (s *SwapLog) Infof(format string, params ...interface{}) { + s.Logger.Infof( + fmt.Sprintf("%v %s", ShortHash(&s.Hash), format), + params..., + ) +} + +// Warnf formats message according to format specifier and writes to +// to log with LevelError. +func (s *SwapLog) Warnf(format string, params ...interface{}) { + s.Logger.Warnf( + fmt.Sprintf("%v %s", ShortHash(&s.Hash), format), + params..., + ) +} + +// Errorf formats message according to format specifier and writes to +// to log with LevelError. +func (s *SwapLog) Errorf(format string, params ...interface{}) { + s.Logger.Errorf( + fmt.Sprintf("%v %s", ShortHash(&s.Hash), format), + params..., + ) + +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..953b233 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,123 @@ +package utils + +import ( + "bytes" + "errors" + "fmt" + "os" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/zpay32" + + "github.com/btcsuite/btcd/wire" +) + +const ( + // FeeRateTotalParts defines the granularity of the fee rate. + FeeRateTotalParts = 1e6 +) + +// ShortHash returns a shortened version of the hash suitable for use in +// logging. +func ShortHash(hash *lntypes.Hash) string { + return hash.String()[:6] +} + +// EncodeTx encodes a tx to raw bytes. +func EncodeTx(tx *wire.MsgTx) ([]byte, error) { + var buffer bytes.Buffer + err := tx.BtcEncode(&buffer, 0, wire.WitnessEncoding) + if err != nil { + return nil, err + } + rawTx := buffer.Bytes() + + return rawTx, nil +} + +// DecodeTx decodes raw tx bytes. +func DecodeTx(rawTx []byte) (*wire.MsgTx, error) { + tx := wire.MsgTx{} + r := bytes.NewReader(rawTx) + err := tx.BtcDecode(r, 0, wire.WitnessEncoding) + if err != nil { + return nil, err + } + + return &tx, nil +} + +// GetInvoiceAmt gets the invoice amount. It requires an amount to be specified. +func GetInvoiceAmt(params *chaincfg.Params, + payReq string) (btcutil.Amount, error) { + + swapPayReq, err := zpay32.Decode( + payReq, params, + ) + if err != nil { + return 0, err + } + + if swapPayReq.MilliSat == nil { + return 0, errors.New("no amount in invoice") + } + + return swapPayReq.MilliSat.ToSatoshis(), nil +} + +// FileExists returns true if the file exists, and false otherwise. +func FileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + + return true +} + +// ChainParamsFromNetwork returns chain parameters based on a network name. +func ChainParamsFromNetwork(network string) (*chaincfg.Params, error) { + switch network { + case "mainnet": + return &chaincfg.MainNetParams, nil + case "testnet": + return &chaincfg.TestNet3Params, nil + case "regtest": + return &chaincfg.RegressionNetParams, nil + case "simnet": + return &chaincfg.SimNetParams, nil + default: + return nil, errors.New("unknown network") + } +} + +// GetScriptOutput locates the given script in the outputs of a transaction and +// returns its outpoint and value. +func GetScriptOutput(htlcTx *wire.MsgTx, scriptHash []byte) ( + *wire.OutPoint, btcutil.Amount, error) { + + for idx, output := range htlcTx.TxOut { + if bytes.Equal(output.PkScript, scriptHash) { + return &wire.OutPoint{ + Hash: htlcTx.TxHash(), + Index: uint32(idx), + }, btcutil.Amount(output.Value), nil + } + } + + return nil, 0, fmt.Errorf("cannot determine outpoint") +} + +// CalcFee returns the swap fee for a given swap amount. +func CalcFee(amount, feeBase btcutil.Amount, feeRate int64) btcutil.Amount { + return feeBase + amount*btcutil.Amount(feeRate)/ + btcutil.Amount(FeeRateTotalParts) +} + +// FeeRateAsPercentage converts a feerate to a percentage. +func FeeRateAsPercentage(feeRate int64) float64 { + return float64(feeRate) / (FeeRateTotalParts / 100) +}