From 30c7d71c5723c35d69c3f92ad86225f9be1eb141 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 4 Apr 2019 12:20:45 +0200 Subject: [PATCH] use np2wsh for loop in htlc --- client.go | 18 +--- cmd/loopd/swapclient_server.go | 16 +-- cmd/loopd/view.go | 66 ++++++++++--- interface.go | 2 + loopin.go | 14 +-- loopin_test.go | 4 +- loopout.go | 14 +-- swap.go | 16 ++- swap/htlc.go | 174 ++++++++++++++++++++++++--------- sweep/sweeper.go | 10 +- 10 files changed, 220 insertions(+), 114 deletions(-) diff --git a/client.go b/client.go index 0fa3b6a..82de7aa 100644 --- a/client.go +++ b/client.go @@ -276,15 +276,9 @@ func (s *Client) LoopOut(globalCtx context.Context, // Post swap to the main loop. s.executor.initiateSwap(globalCtx, swap) - // Retrieve htlc address. - htlcAddress, err := swap.htlc.Address(s.lndServices.ChainParams) - if err != nil { - return nil, nil, err - } - // Return hash so that the caller can identify this swap in the updates // stream. - return &swap.hash, htlcAddress, nil + return &swap.hash, swap.htlc.Address, nil } // LoopOutQuote takes a LoopOut amount and returns a break down of estimated @@ -313,7 +307,7 @@ func (s *Client) LoopOutQuote(ctx context.Context, ) minerFee, err := s.sweeper.GetSweepFee( - ctx, swap.QuoteHtlc.MaxSuccessWitnessSize, + ctx, swap.QuoteHtlc.AddSuccessToEstimator, request.SweepConfTarget, ) if err != nil { @@ -381,15 +375,9 @@ func (s *Client) LoopIn(globalCtx context.Context, // Post swap to the main loop. s.executor.initiateSwap(globalCtx, swap) - // Retrieve htlc address. - htlcAddress, err := swap.htlc.Address(s.lndServices.ChainParams) - if err != nil { - return nil, nil, err - } - // Return hash so that the caller can identify this swap in the updates // stream. - return &swap.hash, htlcAddress, nil + return &swap.hash, swap.htlc.Address, nil } // LoopInQuote takes an amount and returns a break down of estimated diff --git a/cmd/loopd/swapclient_server.go b/cmd/loopd/swapclient_server.go index d0b9650..38179d6 100644 --- a/cmd/loopd/swapclient_server.go +++ b/cmd/loopd/swapclient_server.go @@ -13,7 +13,6 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" - "github.com/lightninglabs/loop/swap" "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/looprpc" @@ -104,19 +103,6 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) ( state = looprpc.SwapState_FAILED } - htlc, err := swap.NewHtlc( - loopSwap.CltvExpiry, loopSwap.SenderKey, loopSwap.ReceiverKey, - loopSwap.SwapHash, - ) - if err != nil { - return nil, err - } - - address, err := htlc.Address(s.lnd.ChainParams) - if err != nil { - return nil, err - } - var swapType looprpc.SwapType switch loopSwap.SwapType { case loop.TypeIn: @@ -133,7 +119,7 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) ( State: state, InitiationTime: loopSwap.InitiationTime.UnixNano(), LastUpdateTime: loopSwap.LastUpdate.UnixNano(), - HtlcAddress: address.EncodeAddress(), + HtlcAddress: loopSwap.HtlcAddress.EncodeAddress(), Type: swapType, }, nil } diff --git a/cmd/loopd/view.go b/cmd/loopd/view.go index 40101c1..e6f6b84 100644 --- a/cmd/loopd/view.go +++ b/cmd/loopd/view.go @@ -4,6 +4,8 @@ import ( "fmt" "strconv" + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/swap" ) @@ -28,13 +30,21 @@ func view(config *config) error { } defer cleanup() - swaps, err := swapClient.FetchLoopOutSwaps() - if err != nil { + if err := viewOut(swapClient, chainParams); err != nil { + return err + } + + if err := viewIn(swapClient, chainParams); err != nil { return err } - if len(swaps) == 0 { - fmt.Printf("No swaps\n") + return nil +} + +func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error { + swaps, err := swapClient.FetchLoopOutSwaps() + if err != nil { + return err } for _, s := range swaps { @@ -42,23 +52,18 @@ func view(config *config) error { s.Contract.CltvExpiry, s.Contract.SenderKey, s.Contract.ReceiverKey, - s.Hash, + s.Hash, swap.HtlcP2WSH, chainParams, ) if err != nil { return err } - htlcAddress, err := htlc.Address(chainParams) - if err != nil { - return err - } - - fmt.Printf("%v\n", s.Hash) + fmt.Printf("OUT %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) + fmt.Printf(" Htlc address: %v\n", htlc.Address) unchargeChannel := "any" if s.Contract.UnchargeChannel != nil { @@ -81,3 +86,40 @@ func view(config *config) error { return nil } + +func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error { + swaps, err := swapClient.FetchLoopInSwaps() + if err != nil { + return err + } + + for _, s := range swaps { + htlc, err := swap.NewHtlc( + s.Contract.CltvExpiry, + s.Contract.SenderKey, + s.Contract.ReceiverKey, + s.Hash, swap.HtlcNP2WSH, chainParams, + ) + if err != nil { + return err + } + + fmt.Printf("IN %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", htlc.Address) + 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/interface.go b/interface.go index 4c2ae45..15d8245 100644 --- a/interface.go +++ b/interface.go @@ -289,6 +289,8 @@ type SwapInfo struct { SwapType Type loopdb.SwapContract + + HtlcAddress btcutil.Address } // LastUpdate returns the last update time of the swap diff --git a/loopin.go b/loopin.go index 53ae6e6..050c180 100644 --- a/loopin.go +++ b/loopin.go @@ -151,7 +151,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig, } swapKit, err := newSwapKit( - swapHash, TypeIn, cfg, &contract.SwapContract, + swapHash, TypeIn, cfg, &contract.SwapContract, swap.HtlcNP2WSH, ) if err != nil { return nil, err @@ -184,7 +184,7 @@ func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig, logger.Infof("Resuming loop in swap %v", hash) swapKit, err := newSwapKit( - hash, TypeIn, cfg, &pend.Contract.SwapContract, + hash, TypeIn, cfg, &pend.Contract.SwapContract, swap.HtlcNP2WSH, ) if err != nil { return nil, err @@ -307,7 +307,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error { // Determine the htlc outpoint by inspecting the htlc tx. htlcOutpoint, htlcValue, err := swap.GetScriptOutput( - conf.Tx, s.htlc.ScriptHash, + conf.Tx, s.htlc.PkScript, ) if err != nil { return err @@ -340,7 +340,7 @@ func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) ( ctx, cancel := context.WithCancel(globalCtx) defer cancel() confChan, confErr, err := s.lnd.ChainNotifier.RegisterConfirmationsNtfn( - ctx, nil, s.htlc.ScriptHash, 1, s.InitiationHeight, + ctx, nil, s.htlc.PkScript, 1, s.InitiationHeight, ) if err != nil { return nil, err @@ -400,7 +400,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) { s.log.Infof("Publishing on chain HTLC with fee rate %v", feeRate) tx, err := s.lnd.WalletKit.SendOutputs(ctx, []*wire.TxOut{{ - PkScript: s.htlc.ScriptHash, + PkScript: s.htlc.PkScript, Value: int64(s.LoopInContract.AmountRequested), }}, feeRate, @@ -424,7 +424,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context, rpcCtx, cancel := context.WithCancel(ctx) defer cancel() spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( - rpcCtx, nil, s.htlc.ScriptHash, s.InitiationHeight, + rpcCtx, nil, s.htlc.PkScript, s.InitiationHeight, ) if err != nil { return fmt.Errorf("register spend ntfn: %v", err) @@ -583,7 +583,7 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context, // Calculate sweep tx fee fee, err := s.sweeper.GetSweepFee( - ctx, s.htlc.MaxTimeoutWitnessSize, TimeoutTxConfTarget, + ctx, s.htlc.AddTimeoutToEstimator, TimeoutTxConfTarget, ) if err != nil { return err diff --git a/loopin_test.go b/loopin_test.go index 197f992..35cc717 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -257,7 +257,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) { htlc, err := swap.NewHtlc( contract.CltvExpiry, contract.SenderKey, contract.ReceiverKey, - testPreimage.Hash(), + testPreimage.Hash(), swap.HtlcNP2WSH, cfg.lnd.ChainParams, ) if err != nil { t.Fatal(err) @@ -329,7 +329,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) { ctx.assertState(loopdb.StateHtlcPublished) htlcTx.AddTxOut(&wire.TxOut{ - PkScript: htlc.ScriptHash, + PkScript: htlc.PkScript, }) } diff --git a/loopout.go b/loopout.go index 8a46734..a7dcacf 100644 --- a/loopout.go +++ b/loopout.go @@ -105,7 +105,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, } swapKit, err := newSwapKit( - swapHash, TypeOut, cfg, &contract.SwapContract, + swapHash, TypeOut, cfg, &contract.SwapContract, swap.HtlcP2WSH, ) if err != nil { return nil, err @@ -138,7 +138,7 @@ func resumeLoopOutSwap(reqContext context.Context, cfg *swapConfig, logger.Infof("Resuming loop out swap %v", hash) swapKit, err := newSwapKit( - hash, TypeOut, cfg, &pend.Contract.SwapContract, + hash, TypeOut, cfg, &pend.Contract.SwapContract, swap.HtlcP2WSH, ) if err != nil { return nil, err @@ -168,6 +168,7 @@ func (s *loopOutSwap) execute(mainCtx context.Context, s.executeConfig = *cfg s.height = height + // Execute swap. err := s.executeAndFinalize(mainCtx) // If an unexpected error happened, report a temporary failure. @@ -189,6 +190,7 @@ func (s *loopOutSwap) execute(mainCtx context.Context, // executeAndFinalize executes a swap and awaits the definitive outcome of the // offchain payments. When this method returns, the swap outcome is final. func (s *loopOutSwap) executeAndFinalize(globalCtx context.Context) error { + // Announce swap by sending out an initial update. err := s.sendUpdate(globalCtx) if err != nil { @@ -281,7 +283,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { // Retrieve outpoint for sweep. htlcOutpoint, htlcValue, err := swap.GetScriptOutput( - txConf.Tx, s.htlc.ScriptHash, + txConf.Tx, s.htlc.PkScript, ) if err != nil { return err @@ -383,7 +385,7 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( defer cancel() htlcConfChan, htlcErrChan, err := s.lnd.ChainNotifier.RegisterConfirmationsNtfn( - ctx, nil, s.htlc.ScriptHash, 1, + ctx, nil, s.htlc.PkScript, 1, s.InitiationHeight, ) if err != nil { @@ -514,7 +516,7 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, ctx, cancel := context.WithCancel(globalCtx) defer cancel() spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn( - ctx, nil, s.htlc.ScriptHash, s.InitiationHeight, + ctx, nil, s.htlc.PkScript, s.InitiationHeight, ) if err != nil { return nil, fmt.Errorf("register spend ntfn: %v", err) @@ -571,7 +573,7 @@ func (s *loopOutSwap) sweep(ctx context.Context, // Calculate sweep tx fee fee, err := s.sweeper.GetSweepFee( - ctx, s.htlc.MaxSuccessWitnessSize, + ctx, s.htlc.AddSuccessToEstimator, s.SweepConfTarget, ) if err != nil { diff --git a/swap.go b/swap.go index 31f2a5d..74a0276 100644 --- a/swap.go +++ b/swap.go @@ -29,29 +29,26 @@ type swapKit struct { } func newSwapKit(hash lntypes.Hash, swapType Type, cfg *swapConfig, - contract *loopdb.SwapContract) (*swapKit, error) { + contract *loopdb.SwapContract, outputType swap.HtlcOutputType) ( + *swapKit, error) { // Compose expected on-chain swap script htlc, err := swap.NewHtlc( contract.CltvExpiry, contract.SenderKey, - contract.ReceiverKey, hash, + contract.ReceiverKey, hash, outputType, + cfg.lnd.ChainParams, ) 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 := &SwapLog{ Hash: hash, Logger: logger, } - log.Infof("Htlc address: %v", htlcAddress) + // Log htlc address for debugging. + log.Infof("Htlc address: %v", htlc.Address) return &swapKit{ swapConfig: *cfg, @@ -72,6 +69,7 @@ func (s *swapKit) sendUpdate(ctx context.Context) error { SwapType: s.swapType, LastUpdate: s.lastUpdateTime, State: s.state, + HtlcAddress: s.htlc.Address, } s.log.Infof("state %v", info.State) diff --git a/swap/htlc.go b/swap/htlc.go index 380e4ea..02a6510 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -2,6 +2,7 @@ package swap import ( "bytes" + "crypto/sha256" "errors" "github.com/btcsuite/btcd/chaincfg" @@ -12,13 +13,27 @@ import ( "github.com/lightningnetwork/lnd/lntypes" ) +// HtlcOutputType defines the output type of the htlc that is published. +type HtlcOutputType uint8 + +const ( + // HtlcP2WSH is a pay-to-witness-script-hash output (segwit only) + HtlcP2WSH HtlcOutputType = iota + + // HtlcNP2WSH is a nested pay-to-witness-script-hash output that can be + // paid to be legacy wallets. + HtlcNP2WSH +) + // Htlc contains relevant htlc information from the receiver perspective. type Htlc struct { - Script []byte - ScriptHash []byte - Hash lntypes.Hash - MaxSuccessWitnessSize int - MaxTimeoutWitnessSize int + Script []byte + PkScript []byte + Hash lntypes.Hash + OutputType HtlcOutputType + ChainParams *chaincfg.Params + Address btcutil.Address + SigScript []byte } var ( @@ -30,13 +45,15 @@ var ( // the maximum value for cltv expiry to get the maximum (worst case) // script size. QuoteHtlc, _ = NewHtlc( - ^int32(0), quoteKey, quoteKey, quoteHash, + ^int32(0), quoteKey, quoteKey, quoteHash, HtlcP2WSH, + &chaincfg.MainNetParams, ) ) // NewHtlc returns a new instance. func NewHtlc(cltvExpiry int32, senderKey, receiverKey [33]byte, - hash lntypes.Hash) (*Htlc, error) { + hash lntypes.Hash, outputType HtlcOutputType, + chainParams *chaincfg.Params) (*Htlc, error) { script, err := swapHTLCScript( cltvExpiry, senderKey, receiverKey, hash, @@ -45,39 +62,73 @@ func NewHtlc(cltvExpiry int32, senderKey, receiverKey [33]byte, return nil, err } - scriptHash, err := input.WitnessScriptHash(script) + p2wshPkScript, 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) + p2wshPkScriptHash := sha256.Sum256(p2wshPkScript) + + var pkScript, sigScript []byte + var address btcutil.Address + + switch outputType { + case HtlcNP2WSH: + // Generate p2sh script for p2wsh (nested). + + hash160 := input.Ripemd160H(p2wshPkScriptHash[:]) + + builder := txscript.NewScriptBuilder() + + builder.AddOp(txscript.OP_HASH160) + builder.AddData(hash160) + builder.AddOp(txscript.OP_EQUAL) + + pkScript, err = builder.Script() + if err != nil { + return nil, err + } + + // Generate a valid sigScript that will allow us to spend the + // p2sh output. The sigScript will contain only a single push of + // the p2wsh witness program corresponding to the matching + // public key of this address. + sigScript, err = txscript.NewScriptBuilder(). + AddData(p2wshPkScript). + Script() + if err != nil { + return nil, err + } + + address, err = btcutil.NewAddressScriptHash( + p2wshPkScript, chainParams, + ) + if err != nil { + return nil, err + } + + case HtlcP2WSH: + pkScript = p2wshPkScript + + address, err = btcutil.NewAddressWitnessScriptHash( + p2wshPkScriptHash[:], + chainParams, + ) + if err != nil { + return nil, err + } + default: + return nil, errors.New("unknown output type") + } return &Htlc{ - Hash: hash, - Script: script, - ScriptHash: scriptHash, - MaxSuccessWitnessSize: maxSuccessWitnessSize, - MaxTimeoutWitnessSize: maxTimeoutWitnessSize, + Hash: hash, + Script: script, + PkScript: pkScript, + OutputType: outputType, + ChainParams: chainParams, + Address: address, + SigScript: sigScript, }, nil } @@ -127,17 +178,6 @@ func swapHTLCScript(cltvExpiry int32, senderHtlcKey, 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, @@ -178,3 +218,47 @@ func (h *Htlc) GenTimeoutWitness(senderSig []byte) (wire.TxWitness, error) { return witnessStack, nil } + +// AddSuccessToEstimator adds a successful spend to a weight estimator. +func (h *Htlc) AddSuccessToEstimator(estimator *input.TxWeightEstimator) { + // 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(h.Script) + + switch h.OutputType { + case HtlcP2WSH: + estimator.AddWitnessInput(maxSuccessWitnessSize) + + case HtlcNP2WSH: + estimator.AddNestedP2WSHInput(maxSuccessWitnessSize) + } +} + +// AddTimeoutToEstimator adds a timeout spend to a weight estimator. +func (h *Htlc) AddTimeoutToEstimator(estimator *input.TxWeightEstimator) { + // 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(h.Script) + + switch h.OutputType { + case HtlcP2WSH: + estimator.AddWitnessInput(maxTimeoutWitnessSize) + + case HtlcNP2WSH: + estimator.AddNestedP2WSHInput(maxTimeoutWitnessSize) + } +} diff --git a/sweep/sweeper.go b/sweep/sweeper.go index af5bc10..42f5338 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -36,6 +36,7 @@ func (s *Sweeper) CreateSweepTx( // Add HTLC input. sweepTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: htlcOutpoint, + SignatureScript: htlc.SigScript, }) // Add output for the destination address. @@ -85,9 +86,12 @@ func (s *Sweeper) CreateSweepTx( return sweepTx, nil } -// GetSweepFee calculates the required tx fee. +// GetSweepFee calculates the required tx fee to spend to P2WKH. It takes a +// function that is expected to add the weight of the input to the weight +// estimator. func (s *Sweeper) GetSweepFee(ctx context.Context, - htlcSuccessWitnessSize int, sweepConfTarget int32) ( + addInputEstimate func(*input.TxWeightEstimator), + sweepConfTarget int32) ( btcutil.Amount, error) { // Get fee estimate from lnd. @@ -99,7 +103,7 @@ func (s *Sweeper) GetSweepFee(ctx context.Context, // Calculate weight for this tx. var weightEstimate input.TxWeightEstimator weightEstimate.AddP2WKHOutput() - weightEstimate.AddWitnessInput(htlcSuccessWitnessSize) + addInputEstimate(&weightEstimate) weight := weightEstimate.Weight() return feeRate.FeeForWeight(int64(weight)), nil