From 02dd8d23fd1a18c12decf03a22b9c2ca6d2e65e7 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:44 +0100 Subject: [PATCH 01/31] input/size: remove unused constants Since we never attempt to sweep an HTLC we offered with the preimage on the remote's commitment, we never use the constant AcceptedHtlcSuccessWitnessSize for weight estimation. Similarly, we never timout an HTLC offered by the remote on our own commitment, and don't need the constant OfferedHtlcTimeoutWitnessSize. --- input/size.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/input/size.go b/input/size.go index 68fa85e4..b0d28aa2 100644 --- a/input/size.go +++ b/input/size.go @@ -260,19 +260,6 @@ const ( // - witness_script: (accepted_htlc_script) AcceptedHtlcTimeoutWitnessSize = 1 + 1 + 73 + 1 + 1 + AcceptedHtlcScriptSize - // AcceptedHtlcSuccessWitnessSize 322 bytes - // - number_of_witness_elements: 1 byte - // - nil_length: 1 byte - // - sig_alice_length: 1 byte - // - sig_alice: 73 bytes - // - sig_bob_length: 1 byte - // - sig_bob: 73 bytes - // - preimage_length: 1 byte - // - preimage: 32 bytes - // - witness_script_length: 1 byte - // - witness_script (accepted_htlc_script) - AcceptedHtlcSuccessWitnessSize = 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 + AcceptedHtlcScriptSize - // AcceptedHtlcPenaltyWitnessSize 249 bytes // - number_of_witness_elements: 1 byte // - revocation_sig_length: 1 byte @@ -317,18 +304,6 @@ const ( // - OP_ENDIF: 1 byte OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 4*1 - // OfferedHtlcTimeoutWitnessSize 285 bytes - // - number_of_witness_elements: 1 byte - // - nil_length: 1 byte - // - sig_alice_length: 1 byte - // - sig_alice: 73 bytes - // - sig_bob_length: 1 byte - // - sig_bob: 73 bytes - // - nil_length: 1 byte - // - witness_script_length: 1 byte - // - witness_script (offered_htlc_script) - OfferedHtlcTimeoutWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 + OfferedHtlcScriptSize - // OfferedHtlcSuccessWitnessSize 317 bytes // - number_of_witness_elements: 1 byte // - nil_length: 1 byte From 865776c7763833800d375285b7ea9772edf8e675 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:44 +0100 Subject: [PATCH 02/31] lnwallet/channel: use genHtlcScript to create scripts It takes into account the necessary variables and will prepare us for doing commitment type dependent script generation later. --- lnwallet/channel.go | 111 +++++++++++++------------------------------- 1 file changed, 33 insertions(+), 78 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index fa96f8c4..c60c591e 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2157,11 +2157,6 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // remote commitment transaction. htlcRetributions := make([]HtlcRetribution, 0, len(revokedSnapshot.Htlcs)) for _, htlc := range revokedSnapshot.Htlcs { - var ( - htlcWitnessScript []byte - err error - ) - // If the HTLC is dust, then we'll skip it as it doesn't have // an output on the commitment transaction. if htlcIsDust( @@ -2185,31 +2180,13 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // If this is an incoming HTLC, then this means that they were // the sender of the HTLC (relative to us). So we'll - // re-generate the sender HTLC script. - if htlc.Incoming { - htlcWitnessScript, err = input.SenderHTLCScript( - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } - - } else { - // Otherwise, is this was an outgoing HTLC that we - // sent, then from the PoV of the remote commitment - // state, they're the receiver of this HTLC. - htlcWitnessScript, err = input.ReceiverHTLCScript( - htlc.RefundTimeout, keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, - htlc.RHash[:], - ) - if err != nil { - return nil, err - } - } - - htlcPkScript, err := input.WitnessScriptHash(htlcWitnessScript) + // re-generate the sender HTLC script. Otherwise, is this was + // an outgoing HTLC that we sent, then from the PoV of the + // remote commitment state, they're the receiver of this HTLC. + htlcPkScript, htlcWitnessScript, err := genHtlcScript( + htlc.Incoming, false, htlc.RefundTimeout, + htlc.RHash, keyRing, + ) if err != nil { return nil, err } @@ -5325,24 +5302,19 @@ func newOutgoingHtlcResolution(signer input.Signer, Index: uint32(htlc.OutputIndex), } + // First, we'll re-generate the script used to send the HTLC to + // the remote party within their commitment transaction. + htlcScriptHash, htlcScript, err := genHtlcScript( + false, localCommit, htlc.RefundTimeout, htlc.RHash, keyRing, + ) + if err != nil { + return nil, err + } + // If we're spending this HTLC output from the remote node's // commitment, then we won't need to go to the second level as our // outputs don't have a CSV delay. if !localCommit { - // First, we'll re-generate the script used to send the HTLC to - // the remote party within their commitment transaction. - htlcReceiverScript, err := input.ReceiverHTLCScript(htlc.RefundTimeout, - keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } - htlcScriptHash, err := input.WitnessScriptHash(htlcReceiverScript) - if err != nil { - return nil, err - } - // With the script generated, we can completely populated the // SignDescriptor needed to sweep the output. return &OutgoingHtlcResolution{ @@ -5351,7 +5323,7 @@ func newOutgoingHtlcResolution(signer input.Signer, SweepSignDesc: input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcReceiverScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ PkScript: htlcScriptHash, Value: int64(htlc.Amt.ToSatoshis()), @@ -5383,15 +5355,10 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the transaction created, we can generate a sign descriptor // that's capable of generating the signature required to spend the // HTLC output using the timeout transaction. - htlcCreationScript, err := input.SenderHTLCScript(keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, htlc.RHash[:]) - if err != nil { - return nil, err - } timeoutSignDesc := input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcCreationScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ Value: int64(htlc.Amt.ToSatoshis()), }, @@ -5419,7 +5386,7 @@ func newOutgoingHtlcResolution(signer input.Signer, if err != nil { return nil, err } - htlcScriptHash, err := input.WitnessScriptHash(htlcSweepScript) + htlcSweepScriptHash, err := input.WitnessScriptHash(htlcSweepScript) if err != nil { return nil, err } @@ -5440,7 +5407,7 @@ func newOutgoingHtlcResolution(signer input.Signer, SingleTweak: localDelayTweak, WitnessScript: htlcSweepScript, Output: &wire.TxOut{ - PkScript: htlcScriptHash, + PkScript: htlcSweepScriptHash, Value: int64(secondLevelOutputAmt), }, HashType: txscript.SigHashAll, @@ -5465,23 +5432,18 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan Index: uint32(htlc.OutputIndex), } + // First, we'll re-generate the script the remote party used to + // send the HTLC to us in their commitment transaction. + htlcScriptHash, htlcScript, err := genHtlcScript( + true, localCommit, htlc.RefundTimeout, htlc.RHash, keyRing, + ) + if err != nil { + return nil, err + } + // If we're spending this output from the remote node's commitment, // then we can skip the second layer and spend the output directly. if !localCommit { - // First, we'll re-generate the script the remote party used to - // send the HTLC to us in their commitment transaction. - htlcSenderScript, err := input.SenderHTLCScript( - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } - htlcScriptHash, err := input.WitnessScriptHash(htlcSenderScript) - if err != nil { - return nil, err - } - // With the script generated, we can completely populated the // SignDescriptor needed to sweep the output. return &IncomingHtlcResolution{ @@ -5490,7 +5452,7 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan SweepSignDesc: input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcSenderScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ PkScript: htlcScriptHash, Value: int64(htlc.Amt.ToSatoshis()), @@ -5516,17 +5478,10 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan // Once we've created the second-level transaction, we'll generate the // SignDesc needed spend the HTLC output using the success transaction. - htlcCreationScript, err := input.ReceiverHTLCScript(htlc.RefundTimeout, - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } successSignDesc := input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcCreationScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ Value: int64(htlc.Amt.ToSatoshis()), }, @@ -5556,7 +5511,7 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan if err != nil { return nil, err } - htlcScriptHash, err := input.WitnessScriptHash(htlcSweepScript) + htlcSweepScriptHash, err := input.WitnessScriptHash(htlcSweepScript) if err != nil { return nil, err } @@ -5576,7 +5531,7 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan SingleTweak: localDelayTweak, WitnessScript: htlcSweepScript, Output: &wire.TxOut{ - PkScript: htlcScriptHash, + PkScript: htlcSweepScriptHash, Value: int64(secondLevelOutputAmt), }, HashType: txscript.SigHashAll, From b56c7e308bb8d3f8a00058b68ba67adacdbbbceb Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:44 +0100 Subject: [PATCH 03/31] input/script_utils_test: extract script execution into assert method This fixes an error case that wouldn't have been caught, since vm.Execute applies more rules than the individual steps (most notably the clean stack rule). Instead we execute the engine as normal, and only step through if we decide that the outcome is unexpected. --- input/script_utils_test.go | 173 ++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 91 deletions(-) diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 1c6c5bc9..b716d4db 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -15,6 +15,73 @@ import ( "github.com/lightningnetwork/lnd/keychain" ) +// assertEngineExecution executes the VM returned by the newEngine closure, +// asserting the result matches the validity expectation. In the case where it +// doesn't match the expectation, it executes the script step-by-step and +// prints debug information to stdout. +func assertEngineExecution(t *testing.T, testNum int, valid bool, + newEngine func() (*txscript.Engine, error)) { + t.Helper() + + // Get a new VM to execute. + vm, err := newEngine() + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + + // Execute the VM, only go on to the step-by-step execution if + // it doesn't validate as expected. + vmErr := vm.Execute() + if valid == (vmErr == nil) { + return + } + + // Now that the execution didn't match what we expected, fetch a new VM + // to step through. + vm, err = newEngine() + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + + // This buffer will trace execution of the Script, dumping out + // to stdout. + var debugBuf bytes.Buffer + + done := false + for !done { + dis, err := vm.DisasmPC() + if err != nil { + t.Fatalf("stepping (%v)\n", err) + } + debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) + + done, err = vm.Step() + if err != nil && valid { + fmt.Println(debugBuf.String()) + t.Fatalf("spend test case #%v failed, spend "+ + "should be valid: %v", testNum, err) + } else if err == nil && !valid && done { + fmt.Println(debugBuf.String()) + t.Fatalf("spend test case #%v succeed, spend "+ + "should be invalid: %v", testNum, err) + } + + debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) + debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) + } + + // If we get to this point the unexpected case was not reached + // during step execution, which happens for some checks, like + // the clean-stack rule. + validity := "invalid" + if valid { + validity = "valid" + } + + fmt.Println(debugBuf.String()) + t.Fatalf("%v spend test case #%v execution ended with: %v", validity, testNum, vmErr) +} + // TestRevocationKeyDerivation tests that given a public key, and a revocation // hash, the homomorphic revocation public and private key derivation work // properly. @@ -308,39 +375,13 @@ func TestHTLCSenderSpendValidation(t *testing.T) { for i, testCase := range testCases { sweepTx.TxIn[0].Witness = testCase.witness() - vm, err := txscript.NewEngine(htlcPkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(paymentAmt)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(htlcPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(paymentAmt)) } - // This buffer will trace execution of the Script, only dumping - // out to stdout in the case that a test fails. - var debugBuf bytes.Buffer - - done := false - for !done { - dis, err := vm.DisasmPC() - if err != nil { - t.Fatalf("stepping (%v)\n", err) - } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) - - done, err = vm.Step() - if err != nil && testCase.valid { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v failed, spend "+ - "should be valid: %v", i, err) - } else if err == nil && !testCase.valid && done { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v succeed, spend "+ - "should be invalid: %v", i, err) - } - - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) - } + assertEngineExecution(t, i, testCase.valid, newEngine) } } @@ -581,37 +622,13 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { for i, testCase := range testCases { sweepTx.TxIn[0].Witness = testCase.witness() - vm, err := txscript.NewEngine(htlcPkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(paymentAmt)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(htlcPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(paymentAmt)) } - // This buffer will trace execution of the Script, only dumping - // out to stdout in the case that a test fails. - var debugBuf bytes.Buffer - - done := false - for !done { - dis, err := vm.DisasmPC() - if err != nil { - t.Fatalf("stepping (%v)\n", err) - } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) - - done, err = vm.Step() - if err != nil && testCase.valid { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v failed, spend should be valid: %v", i, err) - } else if err == nil && !testCase.valid && done { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v succeed, spend should be invalid: %v", i, err) - } - - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) - } + assertEngineExecution(t, i, testCase.valid, newEngine) } } @@ -811,39 +828,13 @@ func TestSecondLevelHtlcSpends(t *testing.T) { for i, testCase := range testCases { sweepTx.TxIn[0].Witness = testCase.witness() - vm, err := txscript.NewEngine(htlcPkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(htlcAmt)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(htlcPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(htlcAmt)) } - // This buffer will trace execution of the Script, only dumping - // out to stdout in the case that a test fails. - var debugBuf bytes.Buffer - - done := false - for !done { - dis, err := vm.DisasmPC() - if err != nil { - t.Fatalf("stepping (%v)\n", err) - } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) - - done, err = vm.Step() - if err != nil && testCase.valid { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v failed, spend "+ - "should be valid: %v", i, err) - } else if err == nil && !testCase.valid && done { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v succeed, spend "+ - "should be invalid: %v", i, err) - } - - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) - } + assertEngineExecution(t, i, testCase.valid, newEngine) } } From b228681a025deeab16fd020095cafdaf966d34be Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 04/31] input/script_utils_test: refactor TestHTLCSenderSpendValidation To be able to change more than the witness used for each test case, we extract commit and sweep tx generation into own methods that can be called from each test case. We do the same for TestHTLCReceiverSpendValidation --- input/script_utils_test.go | 282 +++++++++++++++++++++++-------------- 1 file changed, 173 insertions(+), 109 deletions(-) diff --git a/input/script_utils_test.go b/input/script_utils_test.go index b716d4db..dcfc9d7d 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -212,43 +212,6 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // we'll be using Bob's base point for the revocation key. revocationKey := DeriveRevocationPubkey(bobKeyPub, commitPoint) - // Generate the raw HTLC redemption scripts, and its p2wsh counterpart. - htlcWitnessScript, err := SenderHTLCScript(aliceLocalKey, bobLocalKey, - revocationKey, paymentHash[:]) - if err != nil { - t.Fatalf("unable to create htlc sender script: %v", err) - } - htlcPkScript, err := WitnessScriptHash(htlcWitnessScript) - if err != nil { - t.Fatalf("unable to create p2wsh htlc script: %v", err) - } - - // This will be Alice's commitment transaction. In this scenario Alice - // is sending an HTLC to a node she has a path to (could be Bob, could - // be multiple hops down, it doesn't really matter). - htlcOutput := &wire.TxOut{ - Value: int64(paymentAmt), - PkScript: htlcPkScript, - } - senderCommitTx := wire.NewMsgTx(2) - senderCommitTx.AddTxIn(fakeFundingTxIn) - senderCommitTx.AddTxOut(htlcOutput) - - prevOut := &wire.OutPoint{ - Hash: senderCommitTx.TxHash(), - Index: 0, - } - - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil)) - sweepTx.AddTxOut( - &wire.TxOut{ - PkScript: []byte("doesn't matter"), - Value: 1 * 10e8, - }, - ) - sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) - bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub) aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub) @@ -258,23 +221,81 @@ func TestHTLCSenderSpendValidation(t *testing.T) { bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}} aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} - // We'll also generate a signature on the sweep transaction above - // that will act as Bob's signature to Alice for the second level HTLC - // transaction. - bobSignDesc := SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - PubKey: bobKeyPub, - }, - SingleTweak: bobCommitTweak, - WitnessScript: htlcWitnessScript, - Output: htlcOutput, - HashType: txscript.SigHashAll, - SigHashes: sweepTxSigHashes, - InputIndex: 0, + var ( + htlcWitnessScript, htlcPkScript []byte + htlcOutput *wire.TxOut + sweepTxSigHashes *txscript.TxSigHashes + senderCommitTx, sweepTx *wire.MsgTx + bobRecvrSig []byte + ) + + // genCommitTx generates a commitment tx. + genCommitTx := func() { + // Generate the raw HTLC redemption scripts, and its p2wsh + // counterpart. + htlcWitnessScript, err = SenderHTLCScript( + aliceLocalKey, bobLocalKey, revocationKey, + paymentHash[:], + ) + if err != nil { + t.Fatalf("unable to create htlc sender script: %v", err) + } + htlcPkScript, err = WitnessScriptHash(htlcWitnessScript) + if err != nil { + t.Fatalf("unable to create p2wsh htlc script: %v", err) + } + + // This will be Alice's commitment transaction. In this + // scenario Alice is sending an HTLC to a node she has a path + // to (could be Bob, could be multiple hops down, it doesn't + // really matter). + htlcOutput = &wire.TxOut{ + Value: int64(paymentAmt), + PkScript: htlcPkScript, + } + senderCommitTx = wire.NewMsgTx(2) + senderCommitTx.AddTxIn(fakeFundingTxIn) + senderCommitTx.AddTxOut(htlcOutput) } - bobRecvrSig, err := bobSigner.SignOutputRaw(sweepTx, &bobSignDesc) - if err != nil { - t.Fatalf("unable to generate alice signature: %v", err) + + // genSweepTx generates a sweep of the senderCommitTx. + genSweepTx := func() { + prevOut := &wire.OutPoint{ + Hash: senderCommitTx.TxHash(), + Index: 0, + } + + sweepTx = wire.NewMsgTx(2) + + sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil)) + + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + + sweepTxSigHashes = txscript.NewTxSigHashes(sweepTx) + + // We'll also generate a signature on the sweep transaction above + // that will act as Bob's signature to Alice for the second level HTLC + // transaction. + bobSignDesc := SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + bobRecvrSig, err = bobSigner.SignOutputRaw(sweepTx, &bobSignDesc) + if err != nil { + t.Fatalf("unable to generate alice signature: %v", err) + } } testCases := []struct { @@ -285,6 +306,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // revoke w/ sig // TODO(roasbeef): test invalid revoke makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -305,6 +329,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { { // HTLC with invalid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -328,6 +355,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // HTLC with valid preimage size + sig // TODO(roasbeef): invalid preimage makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -350,6 +380,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // output with the second level HTLC timeout // transaction. makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -441,46 +474,6 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { // be using Alice's base point for the revocation key. revocationKey := DeriveRevocationPubkey(aliceKeyPub, commitPoint) - // Generate the raw HTLC redemption scripts, and its p2wsh counterpart. - htlcWitnessScript, err := ReceiverHTLCScript(cltvTimeout, aliceLocalKey, - bobLocalKey, revocationKey, paymentHash[:]) - if err != nil { - t.Fatalf("unable to create htlc sender script: %v", err) - } - htlcPkScript, err := WitnessScriptHash(htlcWitnessScript) - if err != nil { - t.Fatalf("unable to create p2wsh htlc script: %v", err) - } - - // This will be Bob's commitment transaction. In this scenario Alice is - // sending an HTLC to a node she has a path to (could be Bob, could be - // multiple hops down, it doesn't really matter). - htlcOutput := &wire.TxOut{ - Value: int64(paymentAmt), - PkScript: htlcWitnessScript, - } - - receiverCommitTx := wire.NewMsgTx(2) - receiverCommitTx.AddTxIn(fakeFundingTxIn) - receiverCommitTx.AddTxOut(htlcOutput) - - prevOut := &wire.OutPoint{ - Hash: receiverCommitTx.TxHash(), - Index: 0, - } - - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: *prevOut, - }) - sweepTx.AddTxOut( - &wire.TxOut{ - PkScript: []byte("doesn't matter"), - Value: 1 * 10e8, - }, - ) - sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) - bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub) aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub) @@ -490,23 +483,79 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}} aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} - // We'll also generate a signature on the sweep transaction above - // that will act as Alice's signature to Bob for the second level HTLC - // transaction. - aliceSignDesc := SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - PubKey: aliceKeyPub, - }, - SingleTweak: aliceCommitTweak, - WitnessScript: htlcWitnessScript, - Output: htlcOutput, - HashType: txscript.SigHashAll, - SigHashes: sweepTxSigHashes, - InputIndex: 0, + var ( + htlcWitnessScript, htlcPkScript []byte + htlcOutput *wire.TxOut + receiverCommitTx, sweepTx *wire.MsgTx + sweepTxSigHashes *txscript.TxSigHashes + aliceSenderSig []byte + ) + + genCommitTx := func() { + // Generate the raw HTLC redemption scripts, and its p2wsh + // counterpart. + htlcWitnessScript, err = ReceiverHTLCScript( + cltvTimeout, aliceLocalKey, bobLocalKey, revocationKey, + paymentHash[:], + ) + if err != nil { + t.Fatalf("unable to create htlc sender script: %v", err) + } + htlcPkScript, err = WitnessScriptHash(htlcWitnessScript) + if err != nil { + t.Fatalf("unable to create p2wsh htlc script: %v", err) + } + + // This will be Bob's commitment transaction. In this scenario Alice is + // sending an HTLC to a node she has a path to (could be Bob, could be + // multiple hops down, it doesn't really matter). + htlcOutput = &wire.TxOut{ + Value: int64(paymentAmt), + PkScript: htlcWitnessScript, + } + + receiverCommitTx = wire.NewMsgTx(2) + receiverCommitTx.AddTxIn(fakeFundingTxIn) + receiverCommitTx.AddTxOut(htlcOutput) } - aliceSenderSig, err := aliceSigner.SignOutputRaw(sweepTx, &aliceSignDesc) - if err != nil { - t.Fatalf("unable to generate alice signature: %v", err) + + genSweepTx := func() { + prevOut := &wire.OutPoint{ + Hash: receiverCommitTx.TxHash(), + Index: 0, + } + + sweepTx = wire.NewMsgTx(2) + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *prevOut, + }) + + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + sweepTxSigHashes = txscript.NewTxSigHashes(sweepTx) + + // We'll also generate a signature on the sweep transaction above + // that will act as Alice's signature to Bob for the second level HTLC + // transaction. + aliceSignDesc := SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + aliceSenderSig, err = aliceSigner.SignOutputRaw(sweepTx, &aliceSignDesc) + if err != nil { + t.Fatalf("unable to generate alice signature: %v", err) + } } // TODO(roasbeef): modify valid to check precise script errors? @@ -517,6 +566,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // HTLC redemption w/ invalid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -539,6 +591,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // HTLC redemption w/ valid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -560,6 +615,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // revoke w/ sig makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -580,6 +638,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // refund w/ invalid lock time makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -600,6 +661,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // refund w/ valid lock time makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx() + genSweepTx() + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, From 6ecb379088b5c5c595cf91bf210ae69ab6cd9a93 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 05/31] lnwallet: thread chanType down into genHtlcScript, gen[..]SigJobs, create[..]Tx NO FUNCTIONAL CHANGES This is a commit just supplying the channel types to various methods. --- lnwallet/channel.go | 80 +++++++++++++++++++++-------------- lnwallet/commitment.go | 20 ++++++--- lnwallet/transactions.go | 8 ++-- lnwallet/transactions_test.go | 5 ++- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index c60c591e..79722da8 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -773,6 +773,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, ourWitnessScript, theirWitnessScript []byte pd PaymentDescriptor err error + chanType = lc.channelState.ChanType ) // If the either outputs is dust from the local or remote node's @@ -784,8 +785,9 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit) if !isDustLocal && localCommitKeys != nil { ourP2WSH, ourWitnessScript, err = genHtlcScript( - htlc.Incoming, true, htlc.RefundTimeout, htlc.RHash, - localCommitKeys) + chanType, htlc.Incoming, true, htlc.RefundTimeout, + htlc.RHash, localCommitKeys, + ) if err != nil { return pd, err } @@ -794,8 +796,9 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit) if !isDustRemote && remoteCommitKeys != nil { theirP2WSH, theirWitnessScript, err = genHtlcScript( - htlc.Incoming, false, htlc.RefundTimeout, htlc.RHash, - remoteCommitKeys) + chanType, htlc.Incoming, false, htlc.RefundTimeout, + htlc.RHash, remoteCommitKeys, + ) if err != nil { return pd, err } @@ -1410,7 +1413,8 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, wireMsg.Amount.ToSatoshis(), remoteDustLimit) if !isDustRemote { theirP2WSH, theirWitnessScript, err := genHtlcScript( - false, false, wireMsg.Expiry, wireMsg.PaymentHash, + lc.channelState.ChanType, false, false, + wireMsg.Expiry, wireMsg.PaymentHash, remoteCommitKeys, ) if err != nil { @@ -2184,8 +2188,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // an outgoing HTLC that we sent, then from the PoV of the // remote commitment state, they're the receiver of this HTLC. htlcPkScript, htlcWitnessScript, err := genHtlcScript( - htlc.Incoming, false, htlc.RefundTimeout, - htlc.RHash, keyRing, + chanState.ChanType, htlc.Incoming, false, + htlc.RefundTimeout, htlc.RHash, keyRing, ) if err != nil { return nil, err @@ -2714,6 +2718,7 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, // signature can be submitted to the sigPool to generate all the signatures // asynchronously and in parallel. func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, + chanType channeldb.ChannelType, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, remoteCommitView *commitment) ([]SignJob, chan struct{}, error) { @@ -2761,7 +2766,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Index: uint32(htlc.remoteOutputIndex), } sigJob.Tx, err = createHtlcTimeoutTx( - op, outputAmt, htlc.Timeout, + chanType, op, outputAmt, htlc.Timeout, uint32(remoteChanCfg.CsvDelay), keyRing.RevocationKey, keyRing.ToLocalKey, ) @@ -2813,7 +2818,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Index: uint32(htlc.remoteOutputIndex), } sigJob.Tx, err = createHtlcSuccessTx( - op, outputAmt, uint32(remoteChanCfg.CsvDelay), + chanType, op, outputAmt, uint32(remoteChanCfg.CsvDelay), keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { @@ -3318,7 +3323,8 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, []ch // need to generate signatures of each of them for the remote party's // commitment state. We do so in two phases: first we generate and // submit the set of signature jobs to the worker pool. - sigBatch, cancelChan, err := genRemoteHtlcSigJobs(keyRing, + sigBatch, cancelChan, err := genRemoteHtlcSigJobs( + keyRing, lc.channelState.ChanType, &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, newCommitView, ) @@ -3776,6 +3782,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // directly into the pool of workers. func genHtlcSigValidationJobs(localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, + chanType channeldb.ChannelType, localChanCfg, remoteChanCfg *channeldb.ChannelConfig) ([]VerifyJob, error) { txHash := localCommitmentView.txn.TxHash() @@ -3822,9 +3829,11 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, htlcFee := htlcSuccessFee(feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - successTx, err := createHtlcSuccessTx(op, - outputAmt, uint32(localChanCfg.CsvDelay), - keyRing.RevocationKey, keyRing.ToLocalKey) + successTx, err := createHtlcSuccessTx( + chanType, op, outputAmt, + uint32(localChanCfg.CsvDelay), + keyRing.RevocationKey, keyRing.ToLocalKey, + ) if err != nil { return nil, err } @@ -3874,8 +3883,8 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, htlcFee := htlcTimeoutFee(feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - timeoutTx, err := createHtlcTimeoutTx(op, - outputAmt, htlc.Timeout, + timeoutTx, err := createHtlcTimeoutTx( + chanType, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), keyRing.RevocationKey, keyRing.ToLocalKey, ) @@ -4092,7 +4101,8 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, // generated, we'll submit these jobs to the worker pool. verifyJobs, err := genHtlcSigValidationJobs( localCommitmentView, keyRing, htlcSigs, - &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, + lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, ) if err != nil { return err @@ -5100,6 +5110,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, *commitSpend.SpenderTxHash, + chanState.ChanType, ) if err != nil { return nil, fmt.Errorf("unable to create htlc "+ @@ -5295,7 +5306,7 @@ func newOutgoingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay uint32, - localCommit bool) (*OutgoingHtlcResolution, error) { + localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitHash, @@ -5305,7 +5316,8 @@ func newOutgoingHtlcResolution(signer input.Signer, // First, we'll re-generate the script used to send the HTLC to // the remote party within their commitment transaction. htlcScriptHash, htlcScript, err := genHtlcScript( - false, localCommit, htlc.RefundTimeout, htlc.RHash, keyRing, + chanType, false, localCommit, htlc.RefundTimeout, htlc.RHash, + keyRing, ) if err != nil { return nil, err @@ -5345,8 +5357,8 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the fee calculated, re-construct the second level timeout // transaction. timeoutTx, err := createHtlcTimeoutTx( - op, secondLevelOutputAmt, htlc.RefundTimeout, csvDelay, - keyRing.RevocationKey, keyRing.ToLocalKey, + chanType, op, secondLevelOutputAmt, htlc.RefundTimeout, + csvDelay, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { return nil, err @@ -5422,10 +5434,11 @@ func newOutgoingHtlcResolution(signer input.Signer, // they can just sweep the output immediately with knowledge of the pre-image. // // TODO(roasbeef) consolidate code with above func -func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, - commitHash chainhash.Hash, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, - feePerKw chainfee.SatPerKWeight, csvDelay uint32, - localCommit bool) (*IncomingHtlcResolution, error) { +func newIncomingHtlcResolution(signer input.Signer, + localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash, + htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, + feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool, + chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitHash, @@ -5435,7 +5448,8 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan // First, we'll re-generate the script the remote party used to // send the HTLC to us in their commitment transaction. htlcScriptHash, htlcScript, err := genHtlcScript( - true, localCommit, htlc.RefundTimeout, htlc.RHash, keyRing, + chanType, true, localCommit, htlc.RefundTimeout, htlc.RHash, + keyRing, ) if err != nil { return nil, err @@ -5469,7 +5483,7 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan htlcFee := htlcSuccessFee(feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee successTx, err := createHtlcSuccessTx( - op, secondLevelOutputAmt, csvDelay, + chanType, op, secondLevelOutputAmt, csvDelay, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { @@ -5569,7 +5583,8 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint { func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - commitHash chainhash.Hash) (*HtlcResolutions, error) { + commitHash chainhash.Hash, chanType channeldb.ChannelType) ( + *HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? dustLimit := remoteChanCfg.DustLimit @@ -5582,6 +5597,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, incomingResolutions := make([]IncomingHtlcResolution, 0, len(htlcs)) outgoingResolutions := make([]OutgoingHtlcResolution, 0, len(htlcs)) for _, htlc := range htlcs { + htlc := htlc + // We'll skip any HTLC's which were dust on the commitment // transaction, as these don't have a corresponding output // within the commitment transaction. @@ -5596,8 +5613,9 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, // Otherwise, we'll create an incoming HTLC resolution // as we can satisfy the contract. ihr, err := newIncomingHtlcResolution( - signer, localChanCfg, commitHash, &htlc, keyRing, - feePerKw, uint32(csvDelay), ourCommit, + signer, localChanCfg, commitHash, &htlc, + keyRing, feePerKw, uint32(csvDelay), ourCommit, + chanType, ) if err != nil { return nil, err @@ -5609,7 +5627,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, ohr, err := newOutgoingHtlcResolution( signer, localChanCfg, commitHash, &htlc, keyRing, - feePerKw, uint32(csvDelay), ourCommit, + feePerKw, uint32(csvDelay), ourCommit, chanType, ) if err != nil { return nil, err @@ -5788,7 +5806,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, signer input.Si htlcResolutions, err := extractHtlcResolutions( chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg, - &chanState.RemoteChanCfg, txHash, + &chanState.RemoteChanCfg, txHash, chanState.ChanType, ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 459dd83b..5d56fcd9 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -361,7 +361,10 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, continue } - err := addHTLC(commitTx, isOurs, false, htlc, keyRing) + err := addHTLC( + commitTx, isOurs, false, htlc, keyRing, + cb.chanState.ChanType, + ) if err != nil { return nil, err } @@ -373,7 +376,10 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, continue } - err := addHTLC(commitTx, isOurs, true, htlc, keyRing) + err := addHTLC( + commitTx, isOurs, true, htlc, keyRing, + cb.chanState.ChanType, + ) if err != nil { return nil, err } @@ -485,7 +491,8 @@ func CreateCommitTx(chanType channeldb.ChannelType, // genHtlcScript generates the proper P2WSH public key scripts for the HTLC // output modified by two-bits denoting if this is an incoming HTLC, and if the // HTLC is being applied to their commitment transaction or ours. -func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, +func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, + timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing) ([]byte, []byte, error) { var ( @@ -549,13 +556,14 @@ func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, // the descriptor itself. func addHTLC(commitTx *wire.MsgTx, ourCommit bool, isIncoming bool, paymentDesc *PaymentDescriptor, - keyRing *CommitmentKeyRing) error { + keyRing *CommitmentKeyRing, chanType channeldb.ChannelType) error { timeout := paymentDesc.Timeout rHash := paymentDesc.RHash - p2wsh, witnessScript, err := genHtlcScript(isIncoming, ourCommit, - timeout, rHash, keyRing) + p2wsh, witnessScript, err := genHtlcScript( + chanType, isIncoming, ourCommit, timeout, rHash, keyRing, + ) if err != nil { return err } diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index 7803f752..c8f14a96 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" ) @@ -44,8 +45,8 @@ var ( // In order to spend the HTLC output, the witness for the passed transaction // should be: // * <0> -func createHtlcSuccessTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, - csvDelay uint32, +func createHtlcSuccessTx(chanType channeldb.ChannelType, + htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay uint32, revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this @@ -97,7 +98,8 @@ func createHtlcSuccessTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, // NOTE: The passed amount for the HTLC should take into account the required // fee rate at the time the HTLC was created. The fee should be able to // entirely pay for this (tiny: 1-in 1-out) transaction. -func createHtlcTimeoutTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, +func createHtlcTimeoutTx(chanType channeldb.ChannelType, + htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, cltvExpiry, csvDelay uint32, revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 4f5cc65a..db5a8dce 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -851,9 +851,10 @@ func TestCommitmentAndHTLCTransactions(t *testing.T) { // Generate second-level HTLC transactions for HTLCs in // commitment tx. htlcResolutions, err := extractHtlcResolutions( - chainfee.SatPerKWeight(test.commitment.FeePerKw), true, signer, - htlcs, keys, &channel.channelState.LocalChanCfg, + chainfee.SatPerKWeight(test.commitment.FeePerKw), true, + signer, htlcs, keys, &channel.channelState.LocalChanCfg, &channel.channelState.RemoteChanCfg, commitTx.TxHash(), + channel.channelState.ChanType, ) if err != nil { t.Errorf("Case %d: Failed to extract HTLC resolutions: %v", i, err) From b25f2fa94b2e49ee901c771d87d73d4ac8d358fa Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 06/31] channeldb: define AnchorOutputsBit channel type --- channeldb/channel.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/channeldb/channel.go b/channeldb/channel.go index 0914f242..1ce43ad4 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -176,6 +176,12 @@ const ( // disk. This bit may be on if the funding transaction was crafted by a // wallet external to the primary daemon. NoFundingTxBit ChannelType = 1 << 2 + + // AnchorOutputsBit indicates that the channel makes use of anchor + // outputs to bump the commitment transaction's effective feerate. This + // channel type also uses a delayed to_remote output script. If bit is + // set, we'll find the size of the anchor outputs in the database. + AnchorOutputsBit ChannelType = 1 << 3 ) // IsSingleFunder returns true if the channel type if one of the known single @@ -201,6 +207,12 @@ func (c ChannelType) HasFundingTx() bool { return c&NoFundingTxBit == 0 } +// HasAnchors returns true if this channel type has anchor ouputs on its +// commitment. +func (c ChannelType) HasAnchors() bool { + return c&AnchorOutputsBit == AnchorOutputsBit +} + // ChannelConstraints represents a set of constraints meant to allow a node to // limit their exposure, enact flow control and ensure that all HTLCs are // economically relevant. This struct will be mirrored for both sides of the From 8c0deb81c27d5492e21f33735d4c2ad4bd5c48ce Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 07/31] input+lnwallet: Add scripts for CSV delayed HTLC outputs We also increase the witness size for these types to account for the 3 extra bytes. The size won't be correct in all cases, but it is just an upper bound in any case. --- input/script_utils.go | 29 +++++++++++++++++++++++++++-- input/script_utils_test.go | 4 ++-- input/size.go | 22 ++++++++++++++-------- lnwallet/commitment.go | 8 ++++---- sweep/txgenerator_test.go | 2 +- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index 8b47b056..7342e2e6 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -150,6 +150,9 @@ func Ripemd160H(d []byte) []byte { // * The receiver of the HTLC sweeping all the funds in the case that a // revoked commitment transaction bearing this HTLC was broadcast. // +// If confirmedSpend=true, a 1 OP_CSV check will be added to the non-revocation +// cases, to allow sweeping only after confirmation. +// // Possible Input Scripts: // SENDR: <0> <0> (spend using HTLC timeout transaction) // RECVR: @@ -168,9 +171,11 @@ func Ripemd160H(d []byte) []byte { // OP_HASH160 OP_EQUALVERIFY // OP_CHECKSIG // OP_ENDIF +// [1 OP_CHECKSEQUENCEVERIFY OP_DROP] <- if allowing confirmed spend only. // OP_ENDIF func SenderHTLCScript(senderHtlcKey, receiverHtlcKey, - revocationKey *btcec.PublicKey, paymentHash []byte) ([]byte, error) { + revocationKey *btcec.PublicKey, paymentHash []byte, + confirmedSpend bool) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -243,6 +248,14 @@ func SenderHTLCScript(senderHtlcKey, receiverHtlcKey, // Close out the OP_IF statement above. builder.AddOp(txscript.OP_ENDIF) + // Add 1 block CSV delay if a confirmation is required for the + // non-revocation clauses. + if confirmedSpend { + builder.AddOp(txscript.OP_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + } + // Close out the OP_IF statement at the top of the script. builder.AddOp(txscript.OP_ENDIF) @@ -362,6 +375,9 @@ func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, // * The sender of the HTLC sweeps the HTLC on-chain after the timeout period // of the HTLC has passed. // +// If confirmedSpend=true, a 1 OP_CSV check will be added to the non-revocation +// cases, to allow sweeping only after confirmation. +// // Possible Input Scripts: // RECVR: <0> (spend using HTLC success transaction) // REVOK: @@ -381,10 +397,11 @@ func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, // OP_DROP OP_CHECKLOCKTIMEVERIFY OP_DROP // OP_CHECKSIG // OP_ENDIF +// [1 OP_CHECKSEQUENCEVERIFY OP_DROP] <- if allowing confirmed spend only. // OP_ENDIF func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, - paymentHash []byte) ([]byte, error) { + paymentHash []byte, confirmedSpend bool) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -467,6 +484,14 @@ func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, // Close out the inner if statement. builder.AddOp(txscript.OP_ENDIF) + // Add 1 block CSV delay for non-revocation clauses if confirmation is + // required. + if confirmedSpend { + builder.AddOp(txscript.OP_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + } + // Close out the outer if statement. builder.AddOp(txscript.OP_ENDIF) diff --git a/input/script_utils_test.go b/input/script_utils_test.go index dcfc9d7d..955cfecd 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -235,7 +235,7 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // counterpart. htlcWitnessScript, err = SenderHTLCScript( aliceLocalKey, bobLocalKey, revocationKey, - paymentHash[:], + paymentHash[:], false, ) if err != nil { t.Fatalf("unable to create htlc sender script: %v", err) @@ -496,7 +496,7 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { // counterpart. htlcWitnessScript, err = ReceiverHTLCScript( cltvTimeout, aliceLocalKey, bobLocalKey, revocationKey, - paymentHash[:], + paymentHash[:], false, ) if err != nil { t.Fatalf("unable to create htlc sender script: %v", err) diff --git a/input/size.go b/input/size.go index b0d28aa2..2bea38ce 100644 --- a/input/size.go +++ b/input/size.go @@ -213,7 +213,7 @@ const ( // - witness_script (to_local_script) ToLocalPenaltyWitnessSize = 1 + 1 + 73 + 1 + 1 + ToLocalScriptSize - // AcceptedHtlcScriptSize 139 bytes + // AcceptedHtlcScriptSize 142 bytes // - OP_DUP: 1 byte // - OP_HASH160: 1 byte // - OP_DATA: 1 byte (RIPEMD160(SHA256(revocationkey)) length) @@ -247,11 +247,14 @@ const ( // - OP_DROP: 1 byte // - OP_CHECKSIG: 1 byte // - OP_ENDIF: 1 byte + // - OP_1: 1 byte // These 3 extra bytes are used for both confirmed and regular + // - OP_CSV: 1 byte // HTLC script types. The size won't be correct in all cases, + // - OP_DROP: 1 byte // but it is just an upper bound used for fee estimation in any case. // - OP_ENDIF: 1 byte AcceptedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 7*1 + 20 + 4*1 + - 33 + 5*1 + 4 + 5*1 + 33 + 5*1 + 4 + 8*1 - // AcceptedHtlcTimeoutWitnessSize 216 + // AcceptedHtlcTimeoutWitnessSize 219 // - number_of_witness_elements: 1 byte // - sender_sig_length: 1 byte // - sender_sig: 73 bytes @@ -260,7 +263,7 @@ const ( // - witness_script: (accepted_htlc_script) AcceptedHtlcTimeoutWitnessSize = 1 + 1 + 73 + 1 + 1 + AcceptedHtlcScriptSize - // AcceptedHtlcPenaltyWitnessSize 249 bytes + // AcceptedHtlcPenaltyWitnessSize 252 bytes // - number_of_witness_elements: 1 byte // - revocation_sig_length: 1 byte // - revocation_sig: 73 bytes @@ -270,7 +273,7 @@ const ( // - witness_script (accepted_htlc_script) AcceptedHtlcPenaltyWitnessSize = 1 + 1 + 73 + 1 + 33 + 1 + AcceptedHtlcScriptSize - // OfferedHtlcScriptSize 133 bytes + // OfferedHtlcScriptSize 136 bytes // - OP_DUP: 1 byte // - OP_HASH160: 1 byte // - OP_DATA: 1 byte (RIPEMD160(SHA256(revocationkey)) length) @@ -301,10 +304,13 @@ const ( // - OP_EQUALVERIFY: 1 byte // - OP_CHECKSIG: 1 byte // - OP_ENDIF: 1 byte + // - OP_1: 1 byte + // - OP_CSV: 1 byte + // - OP_DROP: 1 byte // - OP_ENDIF: 1 byte - OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 4*1 + OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 7*1 - // OfferedHtlcSuccessWitnessSize 317 bytes + // OfferedHtlcSuccessWitnessSize 320 bytes // - number_of_witness_elements: 1 byte // - nil_length: 1 byte // - receiver_sig_length: 1 byte @@ -317,7 +323,7 @@ const ( // - witness_script (offered_htlc_script) OfferedHtlcSuccessWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 + OfferedHtlcScriptSize - // OfferedHtlcPenaltyWitnessSize 243 bytes + // OfferedHtlcPenaltyWitnessSize 246 bytes // - number_of_witness_elements: 1 byte // - revocation_sig_length: 1 byte // - revocation_sig: 73 bytes diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 5d56fcd9..2f8b93ac 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -510,28 +510,28 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, case isIncoming && ourCommit: witnessScript, err = input.ReceiverHTLCScript(timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:]) + keyRing.RevocationKey, rHash[:], false) // We're being paid via an HTLC by the remote party, and the HTLC is // being added to their commitment transaction, so we use the sender's // version of the HTLC script. case isIncoming && !ourCommit: witnessScript, err = input.SenderHTLCScript(keyRing.RemoteHtlcKey, - keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:]) + keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:], false) // We're sending an HTLC which is being added to our commitment // transaction. Therefore, we need to use the sender's version of the // HTLC script. case !isIncoming && ourCommit: witnessScript, err = input.SenderHTLCScript(keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:]) + keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], false) // Finally, we're paying the remote party via an HTLC, which is being // added to their commitment transaction. Therefore, we use the // receiver's version of the HTLC script. case !isIncoming && !ourCommit: witnessScript, err = input.ReceiverHTLCScript(timeout, keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:]) + keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], false) } if err != nil { return nil, nil, err diff --git a/sweep/txgenerator_test.go b/sweep/txgenerator_test.go index 84484283..726347ec 100644 --- a/sweep/txgenerator_test.go +++ b/sweep/txgenerator_test.go @@ -14,7 +14,7 @@ var ( input.HtlcOfferedRemoteTimeout, input.WitnessKeyHash, } - expectedWeight = int64(1459) + expectedWeight = int64(1462) expectedSummary = "1 CommitmentTimeLock, 1 " + "HtlcAcceptedSuccessSecondLevel, 1 HtlcOfferedRemoteTimeout, " + "1 WitnessKeyHash" From dc271a80cb046590c63912d69304ee2218eefe98 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 08/31] input/script_utils test: add test cases for delayed HTLC sender script --- input/script_utils_test.go | 148 +++++++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 13 deletions(-) diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 955cfecd..ad7609bb 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -229,13 +229,14 @@ func TestHTLCSenderSpendValidation(t *testing.T) { bobRecvrSig []byte ) - // genCommitTx generates a commitment tx. - genCommitTx := func() { + // genCommitTx generates a commitment tx where the htlc output requires + // confirmation to be spent according to 'confirmed'. + genCommitTx := func(confirmed bool) { // Generate the raw HTLC redemption scripts, and its p2wsh // counterpart. htlcWitnessScript, err = SenderHTLCScript( aliceLocalKey, bobLocalKey, revocationKey, - paymentHash[:], false, + paymentHash[:], confirmed, ) if err != nil { t.Fatalf("unable to create htlc sender script: %v", err) @@ -258,8 +259,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { senderCommitTx.AddTxOut(htlcOutput) } - // genSweepTx generates a sweep of the senderCommitTx. - genSweepTx := func() { + // genSweepTx generates a sweep of the senderCommitTx, and sets the + // sequence if confirmed is true. + genSweepTx := func(confirmed bool) { prevOut := &wire.OutPoint{ Hash: senderCommitTx.TxHash(), Index: 0, @@ -268,6 +270,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { sweepTx = wire.NewMsgTx(2) sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil)) + if confirmed { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1) + } sweepTx.AddTxOut( &wire.TxOut{ @@ -306,8 +311,8 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // revoke w/ sig // TODO(roasbeef): test invalid revoke makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -329,8 +334,8 @@ func TestHTLCSenderSpendValidation(t *testing.T) { { // HTLC with invalid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -355,8 +360,8 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // HTLC with valid preimage size + sig // TODO(roasbeef): invalid preimage makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -375,13 +380,71 @@ func TestHTLCSenderSpendValidation(t *testing.T) { }), true, }, + { + // HTLC with valid preimage size + sig, and with + // enforced locktime in HTLC script. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendRedeem(bobSigner, signDesc, + sweepTx, paymentPreimage) + }), + true, + }, + { + // HTLC with valid preimage size + sig, but trying to + // spend CSV output without sequence set. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendRedeem(bobSigner, signDesc, + sweepTx, paymentPreimage) + }), + false, + }, + { // valid spend to the transition the state of the HTLC // output with the second level HTLC timeout // transaction. makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -400,6 +463,65 @@ func TestHTLCSenderSpendValidation(t *testing.T) { }), true, }, + { + // valid spend to the transition the state of the HTLC + // output with the second level HTLC timeout + // transaction. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendTimeout(bobRecvrSig, aliceSigner, + signDesc, sweepTx) + }), + true, + }, + { + // valid spend to the transition the state of the HTLC + // output with the second level HTLC timeout + // transaction. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendTimeout(bobRecvrSig, aliceSigner, + signDesc, sweepTx) + }), + false, + }, } // TODO(roasbeef): set of cases to ensure able to sign w/ keypath and From a309132253eb52fb4d8ae552ce1fd427f1e30f98 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 09/31] input/script_utils test: add test cases for delayed HTLC receiver scrpts --- input/script_utils_test.go | 146 +++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 13 deletions(-) diff --git a/input/script_utils_test.go b/input/script_utils_test.go index ad7609bb..6f871979 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -613,12 +613,12 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { aliceSenderSig []byte ) - genCommitTx := func() { + genCommitTx := func(confirmed bool) { // Generate the raw HTLC redemption scripts, and its p2wsh // counterpart. htlcWitnessScript, err = ReceiverHTLCScript( cltvTimeout, aliceLocalKey, bobLocalKey, revocationKey, - paymentHash[:], false, + paymentHash[:], confirmed, ) if err != nil { t.Fatalf("unable to create htlc sender script: %v", err) @@ -641,7 +641,7 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { receiverCommitTx.AddTxOut(htlcOutput) } - genSweepTx := func() { + genSweepTx := func(confirmed bool) { prevOut := &wire.OutPoint{ Hash: receiverCommitTx.TxHash(), Index: 0, @@ -651,6 +651,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { sweepTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: *prevOut, }) + if confirmed { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1) + } sweepTx.AddTxOut( &wire.TxOut{ @@ -688,8 +691,8 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // HTLC redemption w/ invalid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -713,8 +716,8 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // HTLC redemption w/ valid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -737,8 +740,8 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // revoke w/ sig makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -757,11 +760,71 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { }), true, }, + { + // HTLC redemption w/ valid preimage size, and with + // enforced locktime in HTLC scripts. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendRedeem(aliceSenderSig, + paymentPreimage, bobSigner, + signDesc, sweepTx) + }), + true, + }, + { + // HTLC redemption w/ valid preimage size, but trying + // to spend CSV output without sequence set. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendRedeem(aliceSenderSig, + paymentPreimage, bobSigner, + signDesc, sweepTx) + }), + false, + }, + { // refund w/ invalid lock time makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -783,8 +846,8 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // refund w/ valid lock time makeWitnessTestCase(t, func() (wire.TxWitness, error) { - genCommitTx() - genSweepTx() + genCommitTx(false) + genSweepTx(false) signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ @@ -803,6 +866,63 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { }), true, }, + { + // refund w/ valid lock time, and enforced locktime in + // HTLC scripts. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendTimeout(aliceSigner, signDesc, + sweepTx, int32(cltvTimeout)) + }), + true, + }, + { + // refund w/ valid lock time, but no sequence set in + // sweep tx trying to spend CSV locked HTLC output. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendTimeout(aliceSigner, signDesc, + sweepTx, int32(cltvTimeout)) + }), + false, + }, } for i, testCase := range testCases { From 990992ce94b473dfbf2091d136468f232aa28513 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:45 +0100 Subject: [PATCH 10/31] input/script_utils: add delayed to_remote script + tests --- input/script_utils.go | 75 +++++++++++++++++++++----- input/script_utils_test.go | 107 +++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 12 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index 7342e2e6..8c650c94 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -853,18 +853,6 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } -// CommitScriptUnencumbered constructs the public key script on the commitment -// transaction paying to the "other" party. The constructed output is a normal -// p2wkh output spendable immediately, requiring no contestation period. -func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { - // This script goes to the "other" party, and is spendable immediately. - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_0) - builder.AddData(btcutil.Hash160(key.SerializeCompressed())) - - return builder.Script() -} - // CommitSpendTimeout constructs a valid witness allowing the owner of a // particular commitment transaction to spend the output returning settled // funds back to themselves after a relative block timeout. In order to @@ -973,6 +961,69 @@ func CommitSpendNoDelay(signer Signer, signDesc *SignDescriptor, return witness, nil } +// CommitScriptUnencumbered constructs the public key script on the commitment +// transaction paying to the "other" party. The constructed output is a normal +// p2wkh output spendable immediately, requiring no contestation period. +func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { + // This script goes to the "other" party, and is spendable immediately. + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(btcutil.Hash160(key.SerializeCompressed())) + + return builder.Script() +} + +// CommitScriptToRemoteConfirmed constructs the script for the output on the +// commitment transaction paying to the remote party of said commitment +// transaction. The money can only be spend after one confirmation. +// +// Possible Input Scripts: +// SWEEP: +// +// Output Script: +// OP_CHECKSIGVERIFY +// 1 OP_CHECKSEQUENCEVERIFY +func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { + builder := txscript.NewScriptBuilder() + + // Only the given key can spend the output. + builder.AddData(key.SerializeCompressed()) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + + // Check that the it has one confirmation. + builder.AddOp(txscript.OP_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + return builder.Script() +} + +// CommitSpendToRemoteConfirmed constructs a valid witness allowing a node to +// spend their settled output on the counterparty's commitment transaction when +// it has one confirmetion. This is used for the anchor channel type. The +// spending key will always be non-tweaked for this output type. +func CommitSpendToRemoteConfirmed(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + if signDesc.KeyDesc.PubKey == nil { + return nil, fmt.Errorf("cannot generate witness with nil " + + "KeyDesc pubkey") + } + + // Similar to non delayed output, only a signature is needed. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // Finally, we'll manually craft the witness. The witness here is the + // signature and the redeem script. + witnessStack := make([][]byte, 2) + witnessStack[0] = append(sweepSig, byte(signDesc.HashType)) + witnessStack[1] = signDesc.WitnessScript + + return witnessStack, nil +} + // SingleTweakBytes computes set of bytes we call the single tweak. The purpose // of the single tweak is to randomize all regular delay and payment base // points. To do this, we generate a hash that binds the commitment point to diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 6f871979..dcec43a1 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -1144,6 +1144,113 @@ func TestSecondLevelHtlcSpends(t *testing.T) { } } +// TestCommitSpendToRemoteConfirmed checks that the delayed version of the +// to_remote version can only be spent by the owner, and after one +// confirmation. +func TestCommitSpendToRemoteConfirmed(t *testing.T) { + t.Parallel() + + const outputVal = btcutil.Amount(2 * 10e8) + + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + testWalletPrivKey) + + txid, err := chainhash.NewHash(testHdSeed.CloneBytes()) + if err != nil { + t.Fatalf("unable to create txid: %v", err) + } + commitOut := &wire.OutPoint{ + Hash: *txid, + Index: 0, + } + commitScript, err := CommitScriptToRemoteConfirmed(aliceKeyPub) + if err != nil { + t.Fatalf("unable to create htlc script: %v", err) + } + commitPkScript, err := WitnessScriptHash(commitScript) + if err != nil { + t.Fatalf("unable to create htlc output: %v", err) + } + + commitOutput := &wire.TxOut{ + PkScript: commitPkScript, + Value: int64(outputVal), + } + + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxIn(wire.NewTxIn(commitOut, nil, nil)) + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + + aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} + + testCases := []struct { + witness func() wire.TxWitness + valid bool + }{ + { + // Alice can spend after the a CSV delay has passed. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1) + sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + WitnessScript: commitScript, + Output: commitOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return CommitSpendToRemoteConfirmed(aliceSigner, signDesc, + sweepTx) + }), + true, + }, + { + // Alice cannot spend output without sequence set. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum + sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + WitnessScript: commitScript, + Output: commitOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return CommitSpendToRemoteConfirmed(aliceSigner, signDesc, + sweepTx) + }), + false, + }, + } + + for i, testCase := range testCases { + sweepTx.TxIn[0].Witness = testCase.witness() + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(commitPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(outputVal)) + } + + assertEngineExecution(t, i, testCase.valid, newEngine) + } +} + // TestSpecificationKeyDerivation implements the test vectors provided in // BOLT-03, Appendix E. func TestSpecificationKeyDerivation(t *testing.T) { From 6deb913a8d944166da6073c18d201651bb3ffff3 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:46 +0100 Subject: [PATCH 11/31] input: add CommitmentToRemoteConfirmed witness type --- input/size.go | 16 ++++++++++++++++ input/witnessgen.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/input/size.go b/input/size.go index 2bea38ce..991ac9cf 100644 --- a/input/size.go +++ b/input/size.go @@ -213,6 +213,22 @@ const ( // - witness_script (to_local_script) ToLocalPenaltyWitnessSize = 1 + 1 + 73 + 1 + 1 + ToLocalScriptSize + // ToRemoteConfirmedScriptSize 37 bytes + // - OP_DATA: 1 byte + // - to_remote_key: 33 bytes + // - OP_CHECKSIGVERIFY: 1 byte + // - OP_1: 1 byte + // - OP_CHECKSEQUENCEVERIFY: 1 byte + ToRemoteConfirmedScriptSize = 1 + 33 + 1 + 1 + 1 + + // ToRemoteConfirmedWitnessSize 113 bytes + // - number_of_witness_elements: 1 byte + // - sig_length: 1 byte + // - sig: 73 bytes + // - witness_script_length: 1 byte + // - witness_script (to_remote_delayed_script) + ToRemoteConfirmedWitnessSize = 1 + 1 + 73 + 1 + ToRemoteConfirmedScriptSize + // AcceptedHtlcScriptSize 142 bytes // - OP_DUP: 1 byte // - OP_HASH160: 1 byte diff --git a/input/witnessgen.go b/input/witnessgen.go index 0a2fafb3..7fc509cb 100644 --- a/input/witnessgen.go +++ b/input/witnessgen.go @@ -48,8 +48,9 @@ type StandardWitnessType uint16 var _ WitnessType = (StandardWitnessType)(0) const ( - // CommitmentTimeLock is a witness that allows us to spend the output of - // a commitment transaction after a relative lock-time lockout. + // CommitmentTimeLock is a witness that allows us to spend our output + // on our local commitment transaction after a relative lock-time + // lockout. CommitmentTimeLock StandardWitnessType = 0 // CommitmentNoDelay is a witness that allows us to spend a settled @@ -119,6 +120,11 @@ const ( // type, but it omits the tweak that randomizes the key we need to // spend with a channel peer supplied set of randomness. CommitSpendNoDelayTweakless StandardWitnessType = 12 + + // CommitmentToRemoteConfirmed is a witness that allows us to spend our + // output on the counterparty's commitment transaction after a + // confirmation. + CommitmentToRemoteConfirmed StandardWitnessType = 13 ) // String returns a human readable version of the target WitnessType. @@ -129,6 +135,9 @@ func (wt StandardWitnessType) String() string { case CommitmentTimeLock: return "CommitmentTimeLock" + case CommitmentToRemoteConfirmed: + return "CommitmentToRemoteConfirmed" + case CommitmentNoDelay: return "CommitmentNoDelay" @@ -197,6 +206,18 @@ func (wt StandardWitnessType) WitnessGenerator(signer Signer, Witness: witness, }, nil + case CommitmentToRemoteConfirmed: + witness, err := CommitSpendToRemoteConfirmed( + signer, desc, tx, + ) + if err != nil { + return nil, err + } + + return &Script{ + Witness: witness, + }, nil + case CommitmentNoDelay: witness, err := CommitSpendNoDelay(signer, desc, tx, false) if err != nil { @@ -323,6 +344,10 @@ func (wt StandardWitnessType) SizeUpperBound() (int, bool, error) { case CommitmentTimeLock: return ToLocalTimeoutWitnessSize, false, nil + // 1 CSV time locked output to us on remote commitment. + case CommitmentToRemoteConfirmed: + return ToRemoteConfirmedWitnessSize, false, nil + // Outgoing second layer HTLC's that have confirmed within the // chain, and the output they produced is now mature enough to // sweep. From 50199aeaf3b758186c564d3b6c7d036bb67272e2 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:46 +0100 Subject: [PATCH 12/31] input/script_utils: add anchor scripts + tests --- input/script_utils.go | 68 +++++++++++++++++++++++ input/script_utils_test.go | 107 +++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 8c650c94..b0c89a15 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1024,6 +1024,74 @@ func CommitSpendToRemoteConfirmed(signer Signer, signDesc *SignDescriptor, return witnessStack, nil } +// CommitScriptAnchor constructs the script for the anchor output spendable by +// the given key immediately, or by anyone after 16 confirmations. +// +// Possible Input Scripts: +// By owner: +// By anyone (after 16 conf): +// +// Output Script: +// OP_CHECKSIG OP_IFDUP +// OP_NOTIF +// OP_16 OP_CSV +// OP_ENDIF +func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { + builder := txscript.NewScriptBuilder() + + // Spend immediately with key. + builder.AddData(key.SerializeCompressed()) + builder.AddOp(txscript.OP_CHECKSIG) + + // Duplicate the value if true, since it will be consumed by the NOTIF. + builder.AddOp(txscript.OP_IFDUP) + + // Otherwise spendable by anyone after 16 confirmations. + builder.AddOp(txscript.OP_NOTIF) + builder.AddOp(txscript.OP_16) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_ENDIF) + + return builder.Script() +} + +// CommitSpendAnchor constructs a valid witness allowing a node to spend their +// anchor output on the commitment transaction using their funding key. This is +// used for the anchor channel type. +func CommitSpendAnchor(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + if signDesc.KeyDesc.PubKey == nil { + return nil, fmt.Errorf("cannot generate witness with nil " + + "KeyDesc pubkey") + } + + // Create a signature. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // The witness here is just a signature and the redeem script. + witnessStack := make([][]byte, 2) + witnessStack[0] = append(sweepSig, byte(signDesc.HashType)) + witnessStack[1] = signDesc.WitnessScript + + return witnessStack, nil +} + +// CommitSpendAnchorAnyone constructs a witness allowing anyone to spend the +// anchor output after it has gotten 16 confirmations. Since no signing is +// required, only knowledge of the redeem script is necessary to spend it. +func CommitSpendAnchorAnyone(script []byte) (wire.TxWitness, error) { + // The witness here is just the redeem script. + witnessStack := make([][]byte, 2) + witnessStack[0] = nil + witnessStack[1] = script + + return witnessStack, nil +} + // SingleTweakBytes computes set of bytes we call the single tweak. The purpose // of the single tweak is to randomize all regular delay and payment base // points. To do this, we generate a hash that binds the commitment point to diff --git a/input/script_utils_test.go b/input/script_utils_test.go index dcec43a1..33b78ee2 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -1251,6 +1251,113 @@ func TestCommitSpendToRemoteConfirmed(t *testing.T) { } } +// TestSpendAnchor checks that we can spend the anchors using the various spend +// paths. +func TestSpendAnchor(t *testing.T) { + t.Parallel() + + const anchorSize = 294 + + // First we'll set up some initial key state for Alice. + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + testWalletPrivKey) + + // Create a fake anchor outpoint that we'll use to generate the + // sweeping transaction. + txid, err := chainhash.NewHash(testHdSeed.CloneBytes()) + if err != nil { + t.Fatalf("unable to create txid: %v", err) + } + anchorOutPoint := &wire.OutPoint{ + Hash: *txid, + Index: 0, + } + + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxIn(wire.NewTxIn(anchorOutPoint, nil, nil)) + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + + // Generate the anchor script that can be spent by Alice immediately, + // or by anyone after 16 blocks. + anchorScript, err := CommitScriptAnchor(aliceKeyPub) + if err != nil { + t.Fatalf("unable to create htlc script: %v", err) + } + anchorPkScript, err := WitnessScriptHash(anchorScript) + if err != nil { + t.Fatalf("unable to create htlc output: %v", err) + } + + anchorOutput := &wire.TxOut{ + PkScript: anchorPkScript, + Value: int64(anchorSize), + } + + // Create mock signer for Alice. + aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} + + testCases := []struct { + witness func() wire.TxWitness + valid bool + }{ + { + // Alice can spend immediately. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum + sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + WitnessScript: anchorScript, + Output: anchorOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return CommitSpendAnchor(aliceSigner, signDesc, + sweepTx) + }), + true, + }, + { + // Anyone can spend after 16 blocks. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 16) + return CommitSpendAnchorAnyone(anchorScript) + }), + true, + }, + { + // Anyone cannot spend before 16 blocks. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 15) + return CommitSpendAnchorAnyone(anchorScript) + }), + false, + }, + } + + for i, testCase := range testCases { + sweepTx.TxIn[0].Witness = testCase.witness() + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(anchorPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(anchorSize)) + } + + assertEngineExecution(t, i, testCase.valid, newEngine) + } +} + // TestSpecificationKeyDerivation implements the test vectors provided in // BOLT-03, Appendix E. func TestSpecificationKeyDerivation(t *testing.T) { From af68ff1640125a7e086ae6ac7da858c5b4ae6620 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:46 +0100 Subject: [PATCH 13/31] lnwallet: add anchor commitmenttype With this commitment type, we'll add extra anchor outputs to the commitment transaction if the anchor channel type is active. --- channeldb/channel.go | 6 +- contractcourt/chain_watcher.go | 5 +- lnwallet/channel.go | 22 +++--- lnwallet/commitment.go | 130 +++++++++++++++++++++++++++++---- lnwallet/transactions_test.go | 2 +- lnwallet/wallet.go | 4 +- 6 files changed, 136 insertions(+), 33 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 1ce43ad4..4b97caa6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -338,13 +338,15 @@ type ChannelCommitment struct { // LocalBalance is the current available settled balance within the // channel directly spendable by us. // - // NOTE: This is the balance *after* subtracting any commitment fee. + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. LocalBalance lnwire.MilliSatoshi // RemoteBalance is the current available settled balance within the // channel directly spendable by the remote node. // - // NOTE: This is the balance *after* subtracting any commitment fee. + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. RemoteBalance lnwire.MilliSatoshi // CommitFee is the amount calculated to be paid in fees for the diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 3012376b..bbb3cbb1 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -351,9 +351,8 @@ func isOurCommitment(localChanCfg, remoteChanCfg channeldb.ChannelConfig, // With the keys derived, we'll construct the remote script that'll be // present if they have a non-dust balance on the commitment. - remoteDelay := uint32(remoteChanCfg.CsvDelay) - remoteScript, err := lnwallet.CommitScriptToRemote( - chanType, remoteDelay, commitKeyRing.ToRemoteKey, + remoteScript, _, err := lnwallet.CommitScriptToRemote( + chanType, commitKeyRing.ToRemoteKey, ) if err != nil { return false, err diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 79722da8..b62985fc 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -503,7 +503,8 @@ type commitment struct { // evaluating all the add/remove/settle log entries before the listed // indexes. // - // NOTE: This is the balance *after* subtracting any commitment fee. + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. ourBalance lnwire.MilliSatoshi theirBalance lnwire.MilliSatoshi @@ -2089,9 +2090,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // Since it is the remote breach we are reconstructing, the output going // to us will be a to-remote script with our local params. - ourDelay := uint32(chanState.LocalChanCfg.CsvDelay) - ourScript, err := CommitScriptToRemote( - chanState.ChanType, ourDelay, keyRing.ToRemoteKey, + ourScript, _, err := CommitScriptToRemote( + chanState.ChanType, keyRing.ToRemoteKey, ) if err != nil { return nil, err @@ -5027,8 +5027,8 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { } // CommitOutputResolution carries the necessary information required to allow -// us to sweep our direct commitment output in the case that either party goes -// to chain. +// us to sweep our commitment output in the case that either party goes to +// chain. type CommitOutputResolution struct { // SelfOutPoint is the full outpoint that points to out pay-to-self // output within the closing commitment transaction. @@ -5040,8 +5040,7 @@ type CommitOutputResolution struct { // MaturityDelay is the relative time-lock, in blocks for all outputs // that pay to the local party within the broadcast commitment - // transaction. This value will be non-zero iff, this output was on our - // commitment transaction. + // transaction. MaturityDelay uint32 } @@ -5122,9 +5121,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si // Before we can generate the proper sign descriptor, we'll need to // locate the output index of our non-delayed output on the commitment // transaction. - localDelay := uint32(chanState.LocalChanCfg.CsvDelay) - selfScript, err := CommitScriptToRemote( - chanState.ChanType, localDelay, keyRing.ToRemoteKey, + selfScript, maturityDelay, err := CommitScriptToRemote( + chanState.ChanType, keyRing.ToRemoteKey, ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -5165,7 +5163,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si }, HashType: txscript.SigHashAll, }, - MaturityDelay: 0, + MaturityDelay: maturityDelay, } } diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 2f8b93ac..e3d98cb4 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -13,6 +13,9 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) +// anchorSize is the constant anchor output size. +const anchorSize = btcutil.Amount(330) + // CommitmentKeyRing holds all derived keys needed to construct commitment and // HTLC transactions. The keys are derived differently depending whether the // commitment transaction is ours or the remote peer's. Private keys associated @@ -183,13 +186,34 @@ type ScriptInfo struct { // CommitScriptToRemote creates the script that will pay to the non-owner of // the commitment transaction, adding a delay to the script based on the -// channel type. -func CommitScriptToRemote(_ channeldb.ChannelType, csvTimeout uint32, - key *btcec.PublicKey) (*ScriptInfo, error) { +// channel type. The second return value is the CSV deleay of the output +// script, what must be satisfied in order to spend the output. +func CommitScriptToRemote(chanType channeldb.ChannelType, + key *btcec.PublicKey) (*ScriptInfo, uint32, error) { + // If this channel type has anchors, we derive the delayed to_remote + // script. + if chanType.HasAnchors() { + script, err := input.CommitScriptToRemoteConfirmed(key) + if err != nil { + return nil, 0, err + } + + p2wsh, err := input.WitnessScriptHash(script) + if err != nil { + return nil, 0, err + } + + return &ScriptInfo{ + PkScript: p2wsh, + WitnessScript: script, + }, 1, nil + } + + // Otherwise the to_remote will be a simple p2wkh. p2wkh, err := input.CommitScriptUnencumbered(key) if err != nil { - return nil, err + return nil, 0, err } // Since this is a regular P2WKH, the WitnessScipt and PkScript should @@ -197,7 +221,47 @@ func CommitScriptToRemote(_ channeldb.ChannelType, csvTimeout uint32, return &ScriptInfo{ WitnessScript: p2wkh, PkScript: p2wkh, - }, nil + }, 0, nil +} + +// CommitScriptAnchors return the scripts to use for the local and remote +// anchor. +func CommitScriptAnchors(localChanCfg, + remoteChanCfg *channeldb.ChannelConfig) (*ScriptInfo, + *ScriptInfo, error) { + + // Helper to create anchor ScriptInfo from key. + anchorScript := func(key *btcec.PublicKey) (*ScriptInfo, error) { + script, err := input.CommitScriptAnchor(key) + if err != nil { + return nil, err + } + + scriptHash, err := input.WitnessScriptHash(script) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: scriptHash, + WitnessScript: script, + }, nil + } + + // Get the script used for the anchor output spendable by the local + // node. + localAnchor, err := anchorScript(localChanCfg.MultiSigKey.PubKey) + if err != nil { + return nil, nil, err + } + + // And the anchor spemdable by the remote node. + remoteAnchor, err := anchorScript(remoteChanCfg.MultiSigKey.PubKey) + if err != nil { + return nil, nil, err + } + + return localAnchor, remoteAnchor, nil } // CommitmentBuilder is a type that wraps the type of channel we are dealing @@ -216,6 +280,11 @@ type CommitmentBuilder struct { // NewCommitmentBuilder creates a new CommitmentBuilder from chanState. func NewCommitmentBuilder(chanState *channeldb.OpenChannel) *CommitmentBuilder { + // The anchor channel type MUST be tweakless. + if chanState.ChanType.HasAnchors() && !chanState.ChanType.IsTweakless() { + panic("invalid channel type combination") + } + return &CommitmentBuilder{ chanState: chanState, obfuscator: createStateHintObfuscator(chanState), @@ -248,8 +317,9 @@ type unsignedCommitmentTx struct { // fee is the total fee of the commitment transaction. fee btcutil.Amount - // ourBalance|theirBalance is the balances of this commitment. This can - // be different than the balances before creating the commitment + // ourBalance|theirBalance are the balances of this commitment *after* + // subtracting commitment fees and anchor outputs. This can be + // different than the balances before creating the commitment // transaction as one party must pay the commitment fee. ourBalance lnwire.MilliSatoshi theirBalance lnwire.MilliSatoshi @@ -258,7 +328,7 @@ type unsignedCommitmentTx struct { // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment -// fees. +// fees, but after anchor outputs. func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, theirBalance lnwire.MilliSatoshi, isOurs bool, feePerKw chainfee.SatPerKWeight, height uint64, @@ -333,12 +403,14 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), + numHTLCs, ) } else { commitTx, err = CreateCommitTx( cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), + numHTLCs, ) } if err != nil { @@ -436,7 +508,8 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, func CreateCommitTx(chanType channeldb.ChannelType, fundingOutput wire.TxIn, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - amountToLocal, amountToRemote btcutil.Amount) (*wire.MsgTx, error) { + amountToLocal, amountToRemote btcutil.Amount, + numHTLCs int64) (*wire.MsgTx, error) { // First, we create the script for the delayed "pay-to-self" output. // This output has 2 main redemption clauses: either we can redeem the @@ -458,8 +531,8 @@ func CreateCommitTx(chanType channeldb.ChannelType, } // Next, we create the script paying to the remote. - toRemoteScript, err := CommitScriptToRemote( - chanType, uint32(remoteChanCfg.CsvDelay), keyRing.ToRemoteKey, + toRemoteScript, _, err := CommitScriptToRemote( + chanType, keyRing.ToRemoteKey, ) if err != nil { return nil, err @@ -472,19 +545,50 @@ func CreateCommitTx(chanType channeldb.ChannelType, commitTx.AddTxIn(&fundingOutput) // Avoid creating dust outputs within the commitment transaction. - if amountToLocal >= localChanCfg.DustLimit { + localOutput := amountToLocal >= localChanCfg.DustLimit + if localOutput { commitTx.AddTxOut(&wire.TxOut{ PkScript: toLocalScriptHash, Value: int64(amountToLocal), }) } - if amountToRemote >= localChanCfg.DustLimit { + + remoteOutput := amountToRemote >= localChanCfg.DustLimit + if remoteOutput { commitTx.AddTxOut(&wire.TxOut{ PkScript: toRemoteScript.PkScript, Value: int64(amountToRemote), }) } + // If this channel type has anchors, we'll also add those. + if chanType.HasAnchors() { + localAnchor, remoteAnchor, err := CommitScriptAnchors( + localChanCfg, remoteChanCfg, + ) + if err != nil { + return nil, err + } + + // Add local anchor output only if we have a commitment output + // or there are HTLCs. + if localOutput || numHTLCs > 0 { + commitTx.AddTxOut(&wire.TxOut{ + PkScript: localAnchor.PkScript, + Value: int64(anchorSize), + }) + } + + // Add anchor output to remote only if they have a commitment + // output or there are HTLCs. + if remoteOutput || numHTLCs > 0 { + commitTx.AddTxOut(&wire.TxOut{ + PkScript: remoteAnchor.PkScript, + Value: int64(anchorSize), + }) + } + } + return commitTx, nil } diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index db5a8dce..7a3f08a2 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -1090,7 +1090,7 @@ func testSpendValidation(t *testing.T, tweakless bool) { } commitmentTx, err := CreateCommitTx( channelType, *fakeFundingTxIn, keyRing, aliceChanCfg, - bobChanCfg, channelBalance, channelBalance, + bobChanCfg, channelBalance, channelBalance, 0, ) if err != nil { t.Fatalf("unable to create commitment transaction: %v", nil) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 5143b9f5..5649b652 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -784,7 +784,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, - theirChanCfg, localBalance, remoteBalance, + theirChanCfg, localBalance, remoteBalance, 0, ) if err != nil { return nil, nil, err @@ -797,7 +797,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, - ourChanCfg, remoteBalance, localBalance, + ourChanCfg, remoteBalance, localBalance, 0, ) if err != nil { return nil, nil, err From 1f28bd80869ebe7e185659f949ff79a012bc4e6f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:46 +0100 Subject: [PATCH 14/31] contractcourt/commit_sweep_resolver: set sweep witness type based on witness script We use the fact that we can tell whether the commit is local or remote by inspecting the witness script. We cannot use the maturity delay anymore, as we can have delayed to_remote outputs also now. Co-authored-by: Joost Jager --- contractcourt/commit_sweep_resolver.go | 36 +++++++++++++++++---- contractcourt/commit_sweep_resolver_test.go | 2 ++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index f2061c0f..fe09dc09 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -6,6 +6,7 @@ import ( "io" "sync" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/input" @@ -172,24 +173,45 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { } } - // We're dealing with our commitment transaction if the delay on the - // resolution isn't zero. - isLocalCommitTx := c.commitResolution.MaturityDelay != 0 + // The output is on our local commitment if the script starts with + // OP_IF for the revocation clause. On the remote commitment it will + // either be a regular P2WKH or a simple sig spend with a CSV delay. + isLocalCommitTx := c.commitResolution.SelfOutputSignDesc.WitnessScript[0] == txscript.OP_IF + isDelayedOutput := c.commitResolution.MaturityDelay != 0 - // There're two types of commitments, those that have tweaks - // for the remote key (us in this case), and those that don't. - // We'll rely on the presence of the commitment tweak to to - // discern which type of commitment this is. + c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput, + isLocalCommitTx) + + // There're three types of commitments, those that have tweaks + // for the remote key (us in this case), those that don't, and a third + // where there is no tweak and the output is delayed. On the local + // commitment our output will always be delayed. We'll rely on the + // presence of the commitment tweak to to discern which type of + // commitment this is. var witnessType input.WitnessType switch { + + // Delayed output to us on our local commitment. case isLocalCommitTx: witnessType = input.CommitmentTimeLock + + // A confirmed output to us on the remote commitment. + case isDelayedOutput: + witnessType = input.CommitmentToRemoteConfirmed + + // A non-delayed output on the remote commitment where the key is + // tweakless. case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil: witnessType = input.CommitSpendNoDelayTweakless + + // A non-delayed output on the remote commitment where the key is + // tweaked. default: witnessType = input.CommitmentNoDelay } + c.log.Infof("Sweeping with witness type: %v", witnessType) + // We'll craft an input with all the information required for // the sweeper to create a fully valid sweeping transaction to // recover these coins. diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 4ca01833..c142167e 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -133,6 +133,7 @@ func TestCommitSweepResolverNoDelay(t *testing.T) { Output: &wire.TxOut{ Value: 100, }, + WitnessScript: []byte{0}, }, } @@ -162,6 +163,7 @@ func TestCommitSweepResolverDelay(t *testing.T) { Output: &wire.TxOut{ Value: amt, }, + WitnessScript: []byte{0}, }, MaturityDelay: 3, SelfOutPoint: outpoint, From ea94dbbe34c8b0adf28651f2c1c6f2efcb1a51e0 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:46 +0100 Subject: [PATCH 15/31] input+lnwallet: use individual commit weight calculations for channel type Based on the channel type, the commitment weight will be calculated. --- input/size.go | 29 +++++++++++++++++++++++++++++ lnwallet/channel.go | 5 +++-- lnwallet/commitment.go | 13 ++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/input/size.go b/input/size.go index 991ac9cf..005c2273 100644 --- a/input/size.go +++ b/input/size.go @@ -122,6 +122,12 @@ const ( // - PkScript (P2WPKH) CommitmentKeyHashOutput = 8 + 1 + P2WPKHSize + // CommitmentAnchorOutput 43 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (P2WSH) + CommitmentAnchorOutput = 8 + 1 + P2WSHSize + // HTLCSize 43 bytes // - Value: 8 bytes // - VarInt: 1 byte (PkScript length) @@ -159,9 +165,32 @@ const ( // WitnessCommitmentTxWeight 224 weight WitnessCommitmentTxWeight = WitnessHeaderSize + WitnessSize + // BaseAnchorCommitmentTxSize 225 + 43 * num-htlc-outputs bytes + // - Version: 4 bytes + // - WitnessHeader <---- part of the witness data + // - CountTxIn: 1 byte + // - TxIn: 41 bytes + // FundingInput + // - CountTxOut: 3 byte + // - TxOut: 4*43 + 43 * num-htlc-outputs bytes + // OutputPayingToThem, + // OutputPayingToUs, + // AnchorPayingToThem, + // AnchorPayingToUs, + // ....HTLCOutputs... + // - LockTime: 4 bytes + BaseAnchorCommitmentTxSize = 4 + 1 + FundingInputSize + 3 + + 2*CommitmentDelayOutput + 2*CommitmentAnchorOutput + 4 + + // BaseAnchorCommitmentTxWeight 900 weight + BaseAnchorCommitmentTxWeight = witnessScaleFactor * BaseAnchorCommitmentTxSize + // CommitWeight 724 weight CommitWeight = BaseCommitmentTxWeight + WitnessCommitmentTxWeight + // AnchorCommitWeight 1124 weight + AnchorCommitWeight = BaseAnchorCommitmentTxWeight + WitnessCommitmentTxWeight + // HTLCWeight 172 weight HTLCWeight = witnessScaleFactor * HTLCSize diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b62985fc..148a5a57 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3772,7 +3772,8 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, totalHtlcWeight += input.HTLCWeight } - totalCommitWeight := input.CommitWeight + totalHtlcWeight + totalCommitWeight := CommitWeight(lc.channelState.ChanType) + + totalHtlcWeight return ourBalance, theirBalance, totalCommitWeight, filteredHTLCView, nil } @@ -6322,7 +6323,7 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn, // CalcFee returns the commitment fee to use for the given // fee rate (fee-per-kw). func (lc *LightningChannel) CalcFee(feeRate chainfee.SatPerKWeight) btcutil.Amount { - return feeRate.FeeForWeight(input.CommitWeight) + return feeRate.FeeForWeight(CommitWeight(lc.channelState.ChanType)) } // MaxFeeRate returns the maximum fee rate given an allocation of the channel diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index e3d98cb4..648dbf37 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -224,6 +224,16 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, }, 0, nil } +// CommitWeight returns the base commitment weight before adding HTLCs. +func CommitWeight(chanType channeldb.ChannelType) int64 { + // If this commitment has anchors, it will be slightly heavier. + if chanType.HasAnchors() { + return input.AnchorCommitWeight + } + + return input.CommitWeight +} + // CommitScriptAnchors return the scripts to use for the local and remote // anchor. func CommitScriptAnchors(localChanCfg, @@ -364,7 +374,8 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // on its total weight. Once we have the total weight, we'll multiply // by the current fee-per-kw, then divide by 1000 to get the proper // fee. - totalCommitWeight := input.CommitWeight + (input.HTLCWeight * numHTLCs) + totalCommitWeight := CommitWeight(cb.chanState.ChanType) + + input.HTLCWeight*numHTLCs // With the weight known, we can now calculate the commitment fee, // ensuring that we account for any dust outputs trimmed above. From 21c5a957bc0365ea69f7eb99d5c32d694b854dba Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:46 +0100 Subject: [PATCH 16/31] lnwallet/channel: add feerate sanity check --- lnwallet/channel.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 148a5a57..d71348a6 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2372,6 +2372,29 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, return nil, err } + // We'll assert that there hasn't been a mistake during fee calculation + // leading to a fee too low. + var totalOut btcutil.Amount + for _, txOut := range commitTx.txn.TxOut { + totalOut += btcutil.Amount(txOut.Value) + } + fee := lc.channelState.Capacity - totalOut + + // Since the transaction is not signed yet, we use the witness weight + // used for weight calculation. + uTx := btcutil.NewTx(commitTx.txn) + weight := blockchain.GetTransactionWeight(uTx) + + input.WitnessCommitmentTxWeight + + effFeeRate := chainfee.SatPerKWeight(fee) * 1000 / + chainfee.SatPerKWeight(weight) + if effFeeRate < chainfee.FeePerKwFloor { + return nil, fmt.Errorf("height=%v, for ChannelPoint(%v) "+ + "attempts to create commitment wigh feerate %v: %v", + nextHeight, lc.channelState.FundingOutpoint, + effFeeRate, spew.Sdump(commitTx)) + } + // With the commitment view created, store the resulting balances and // transaction with the other parameters for this height. c := &commitment{ From 6810912c86de4d48f0de282f1841421ea4f71d2f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:47 +0100 Subject: [PATCH 17/31] lnwallet: choose HTLC scripts based on channel type --- lnwallet/commitment.go | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 648dbf37..ce609f0c 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -615,6 +615,12 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, err error ) + // Choose scripts based on channel type. + confirmedHtlcSpends := false + if chanType.HasAnchors() { + confirmedHtlcSpends = true + } + // Generate the proper redeem scripts for the HTLC output modified by // two-bits denoting if this is an incoming HTLC, and if the HTLC is // being applied to their commitment transaction or ours. @@ -623,30 +629,37 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, // transaction. So we need to use the receiver's version of HTLC the // script. case isIncoming && ourCommit: - witnessScript, err = input.ReceiverHTLCScript(timeout, - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:], false) + witnessScript, err = input.ReceiverHTLCScript( + timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) // We're being paid via an HTLC by the remote party, and the HTLC is // being added to their commitment transaction, so we use the sender's // version of the HTLC script. case isIncoming && !ourCommit: - witnessScript, err = input.SenderHTLCScript(keyRing.RemoteHtlcKey, - keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:], false) + witnessScript, err = input.SenderHTLCScript( + keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) // We're sending an HTLC which is being added to our commitment // transaction. Therefore, we need to use the sender's version of the // HTLC script. case !isIncoming && ourCommit: - witnessScript, err = input.SenderHTLCScript(keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], false) + witnessScript, err = input.SenderHTLCScript( + keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) // Finally, we're paying the remote party via an HTLC, which is being // added to their commitment transaction. Therefore, we use the // receiver's version of the HTLC script. case !isIncoming && !ourCommit: - witnessScript, err = input.ReceiverHTLCScript(timeout, keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], false) + witnessScript, err = input.ReceiverHTLCScript( + timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) } if err != nil { return nil, nil, err From d1089fb449f6c309545323fd17572f1d501cd9c8 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:47 +0100 Subject: [PATCH 18/31] input/test_utils: make mockSigner use SigHashType from sign descriptor --- input/test_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/test_utils.go b/input/test_utils.go index 45c11f11..b9719438 100644 --- a/input/test_utils.go +++ b/input/test_utils.go @@ -67,7 +67,7 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx, signDesc *SignDescriptor) ([] sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, signDesc.InputIndex, signDesc.Output.Value, signDesc.WitnessScript, - txscript.SigHashAll, privKey) + signDesc.HashType, privKey) if err != nil { return nil, err } From bddd3e128cd144f038ac294b37835ad713bd5140 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:47 +0100 Subject: [PATCH 19/31] lnwallet: make second level sigs using sighash single|anyonecanpay --- contractcourt/htlc_timeout_resolver_test.go | 7 ++- input/script_utils.go | 17 +++--- input/script_utils_test.go | 64 +++++++++++++++------ lnwallet/channel.go | 17 ++++-- lnwallet/commitment.go | 11 ++++ 5 files changed, 82 insertions(+), 34 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 1e3daa59..52324698 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/input" @@ -144,7 +145,8 @@ func TestHtlcTimeoutResolver(t *testing.T) { timeout: true, txToBroadcast: func() (*wire.MsgTx, error) { witness, err := input.SenderHtlcSpendTimeout( - nil, signer, fakeSignDesc, sweepTx, + nil, txscript.SigHashAll, signer, + fakeSignDesc, sweepTx, ) if err != nil { return nil, err @@ -163,7 +165,8 @@ func TestHtlcTimeoutResolver(t *testing.T) { timeout: false, txToBroadcast: func() (*wire.MsgTx, error) { witness, err := input.ReceiverHtlcSpendRedeem( - nil, fakePreimageBytes, signer, + nil, txscript.SigHashAll, + fakePreimageBytes, signer, fakeSignDesc, sweepTx, ) if err != nil { diff --git a/input/script_utils.go b/input/script_utils.go index b0c89a15..d8f80f31 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -343,8 +343,10 @@ func SenderHtlcSpendRedeem(signer Signer, signDesc *SignDescriptor, // HTLC to activate the time locked covenant clause of a soon to be expired // HTLC. This script simply spends the multi-sig output using the // pre-generated HTLC timeout transaction. -func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, - signDesc *SignDescriptor, htlcTimeoutTx *wire.MsgTx) (wire.TxWitness, error) { +func SenderHtlcSpendTimeout(receiverSig []byte, + receiverSigHash txscript.SigHashType, signer Signer, + signDesc *SignDescriptor, htlcTimeoutTx *wire.MsgTx) ( + wire.TxWitness, error) { sweepSig, err := signer.SignOutputRaw(htlcTimeoutTx, signDesc) if err != nil { @@ -357,7 +359,7 @@ func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, // original OP_CHECKMULTISIG. witnessStack := wire.TxWitness(make([][]byte, 5)) witnessStack[0] = nil - witnessStack[1] = append(receiverSig, byte(txscript.SigHashAll)) + witnessStack[1] = append(receiverSig, byte(receiverSigHash)) witnessStack[2] = append(sweepSig, byte(signDesc.HashType)) witnessStack[3] = nil witnessStack[4] = signDesc.WitnessScript @@ -506,9 +508,10 @@ func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, // signed has a relative timelock delay enforced by its sequence number. This // delay give the sender of the HTLC enough time to revoke the output if this // is a breach commitment transaction. -func ReceiverHtlcSpendRedeem(senderSig, paymentPreimage []byte, - signer Signer, signDesc *SignDescriptor, - htlcSuccessTx *wire.MsgTx) (wire.TxWitness, error) { +func ReceiverHtlcSpendRedeem(senderSig []byte, + senderSigHash txscript.SigHashType, paymentPreimage []byte, + signer Signer, signDesc *SignDescriptor, htlcSuccessTx *wire.MsgTx) ( + wire.TxWitness, error) { // First, we'll generate a signature for the HTLC success transaction. // The signDesc should be signing with the public key used as the @@ -524,7 +527,7 @@ func ReceiverHtlcSpendRedeem(senderSig, paymentPreimage []byte, // order to consume the extra pop within OP_CHECKMULTISIG. witnessStack := wire.TxWitness(make([][]byte, 5)) witnessStack[0] = nil - witnessStack[1] = append(senderSig, byte(txscript.SigHashAll)) + witnessStack[1] = append(senderSig, byte(senderSigHash)) witnessStack[2] = append(sweepSig, byte(signDesc.HashType)) witnessStack[3] = paymentPreimage witnessStack[4] = signDesc.WitnessScript diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 33b78ee2..be1e2d0f 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -227,6 +227,7 @@ func TestHTLCSenderSpendValidation(t *testing.T) { sweepTxSigHashes *txscript.TxSigHashes senderCommitTx, sweepTx *wire.MsgTx bobRecvrSig []byte + bobSigHash txscript.SigHashType ) // genCommitTx generates a commitment tx where the htlc output requires @@ -260,7 +261,7 @@ func TestHTLCSenderSpendValidation(t *testing.T) { } // genSweepTx generates a sweep of the senderCommitTx, and sets the - // sequence if confirmed is true. + // sequence and sighash single|anyonecanspend if confirmed is true. genSweepTx := func(confirmed bool) { prevOut := &wire.OutPoint{ Hash: senderCommitTx.TxHash(), @@ -283,6 +284,11 @@ func TestHTLCSenderSpendValidation(t *testing.T) { sweepTxSigHashes = txscript.NewTxSigHashes(sweepTx) + bobSigHash = txscript.SigHashAll + if confirmed { + bobSigHash = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + } + // We'll also generate a signature on the sweep transaction above // that will act as Bob's signature to Alice for the second level HTLC // transaction. @@ -293,7 +299,7 @@ func TestHTLCSenderSpendValidation(t *testing.T) { SingleTweak: bobCommitTweak, WitnessScript: htlcWitnessScript, Output: htlcOutput, - HashType: txscript.SigHashAll, + HashType: bobSigHash, SigHashes: sweepTxSigHashes, InputIndex: 0, } @@ -458,8 +464,10 @@ func TestHTLCSenderSpendValidation(t *testing.T) { InputIndex: 0, } - return SenderHtlcSpendTimeout(bobRecvrSig, aliceSigner, - signDesc, sweepTx) + return SenderHtlcSpendTimeout( + bobRecvrSig, bobSigHash, aliceSigner, + signDesc, sweepTx, + ) }), true, }, @@ -487,8 +495,10 @@ func TestHTLCSenderSpendValidation(t *testing.T) { InputIndex: 0, } - return SenderHtlcSpendTimeout(bobRecvrSig, aliceSigner, - signDesc, sweepTx) + return SenderHtlcSpendTimeout( + bobRecvrSig, bobSigHash, aliceSigner, + signDesc, sweepTx, + ) }), true, }, @@ -517,8 +527,10 @@ func TestHTLCSenderSpendValidation(t *testing.T) { InputIndex: 0, } - return SenderHtlcSpendTimeout(bobRecvrSig, aliceSigner, - signDesc, sweepTx) + return SenderHtlcSpendTimeout( + bobRecvrSig, bobSigHash, aliceSigner, + signDesc, sweepTx, + ) }), false, }, @@ -611,6 +623,7 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { receiverCommitTx, sweepTx *wire.MsgTx sweepTxSigHashes *txscript.TxSigHashes aliceSenderSig []byte + aliceSigHash txscript.SigHashType ) genCommitTx := func(confirmed bool) { @@ -663,6 +676,11 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { ) sweepTxSigHashes = txscript.NewTxSigHashes(sweepTx) + aliceSigHash = txscript.SigHashAll + if confirmed { + aliceSigHash = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + } + // We'll also generate a signature on the sweep transaction above // that will act as Alice's signature to Bob for the second level HTLC // transaction. @@ -673,7 +691,7 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { SingleTweak: aliceCommitTweak, WitnessScript: htlcWitnessScript, Output: htlcOutput, - HashType: txscript.SigHashAll, + HashType: aliceSigHash, SigHashes: sweepTxSigHashes, InputIndex: 0, } @@ -706,9 +724,11 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { InputIndex: 0, } - return ReceiverHtlcSpendRedeem(aliceSenderSig, + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, bytes.Repeat([]byte{1}, 45), bobSigner, - signDesc, sweepTx) + signDesc, sweepTx, + ) }), false, @@ -731,9 +751,11 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { InputIndex: 0, } - return ReceiverHtlcSpendRedeem(aliceSenderSig, - paymentPreimage[:], bobSigner, - signDesc, sweepTx) + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, + paymentPreimage, bobSigner, + signDesc, sweepTx, + ) }), true, }, @@ -783,9 +805,11 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { InputIndex: 0, } - return ReceiverHtlcSpendRedeem(aliceSenderSig, + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, paymentPreimage, bobSigner, - signDesc, sweepTx) + signDesc, sweepTx, + ) }), true, }, @@ -813,9 +837,11 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { InputIndex: 0, } - return ReceiverHtlcSpendRedeem(aliceSenderSig, - paymentPreimage, bobSigner, - signDesc, sweepTx) + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, + paymentPreimage, bobSigner, signDesc, + sweepTx, + ) }), false, }, diff --git a/lnwallet/channel.go b/lnwallet/channel.go index d71348a6..dbb2000a 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2748,6 +2748,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, txHash := remoteCommitView.txn.TxHash() dustLimit := remoteChanCfg.DustLimit feePerKw := remoteCommitView.feePerKw + sigHashType := HtlcSigHashType(chanType) // With the keys generated, we'll make a slice with enough capacity to // hold potentially all the HTLCs. The actual slice may be a bit @@ -2807,7 +2808,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Output: &wire.TxOut{ Value: int64(htlc.Amount.ToSatoshis()), }, - HashType: txscript.SigHashAll, + HashType: sigHashType, SigHashes: txscript.NewTxSigHashes(sigJob.Tx), InputIndex: 0, } @@ -2858,7 +2859,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Output: &wire.TxOut{ Value: int64(htlc.Amount.ToSatoshis()), }, - HashType: txscript.SigHashAll, + HashType: sigHashType, SigHashes: txscript.NewTxSigHashes(sigJob.Tx), InputIndex: 0, } @@ -3811,6 +3812,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, txHash := localCommitmentView.txn.TxHash() feePerKw := localCommitmentView.feePerKw + sigHashType := HtlcSigHashType(chanType) // With the required state generated, we'll create a slice with large // enough capacity to hold verification jobs for all HTLC's in this @@ -3865,7 +3867,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, hashCache := txscript.NewTxSigHashes(successTx) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, - txscript.SigHashAll, successTx, 0, + sigHashType, successTx, 0, int64(htlc.Amount.ToSatoshis()), ) if err != nil { @@ -3919,7 +3921,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, hashCache := txscript.NewTxSigHashes(timeoutTx) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, - txscript.SigHashAll, timeoutTx, 0, + sigHashType, timeoutTx, 0, int64(htlc.Amount.ToSatoshis()), ) if err != nil { @@ -5403,8 +5405,9 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the sign desc created, we can now construct the full witness // for the timeout transaction, and populate it as well. + sigHashType := HtlcSigHashType(chanType) timeoutWitness, err := input.SenderHtlcSpendTimeout( - htlc.Signature, signer, &timeoutSignDesc, timeoutTx, + htlc.Signature, sigHashType, signer, &timeoutSignDesc, timeoutTx, ) if err != nil { return nil, err @@ -5530,8 +5533,10 @@ func newIncomingHtlcResolution(signer input.Signer, // the success transaction. Don't specify the preimage yet. The preimage // will be supplied by the contract resolver, either directly or when it // becomes known. + sigHashType := HtlcSigHashType(chanType) successWitness, err := input.ReceiverHtlcSpendRedeem( - htlc.Signature, nil, signer, &successSignDesc, successTx, + htlc.Signature, sigHashType, nil, signer, &successSignDesc, + successTx, ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index ce609f0c..6b1719ee 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -5,6 +5,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/channeldb" @@ -224,6 +225,16 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, }, 0, nil } +// HtlcSigHashType returns the sighash type to use for HTLC success and timeout +// transactions given the channel type. +func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType { + if chanType.HasAnchors() { + return txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + } + + return txscript.SigHashAll +} + // CommitWeight returns the base commitment weight before adding HTLCs. func CommitWeight(chanType channeldb.ChannelType) int64 { // If this commitment has anchors, it will be slightly heavier. From c5d58b4762337b43fddcb11b2e8705e549c9a664 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:47 +0100 Subject: [PATCH 20/31] lnwallet: set 2nd level sequence according to channel type --- lnwallet/commitment.go | 10 ++++++++++ lnwallet/transactions.go | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 6b1719ee..abe9288e 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -235,6 +235,16 @@ func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType { return txscript.SigHashAll } +// HtlcSecondLevelInputSequence dictates the sequence number we must use on the +// input to a second level HTLC transaction. +func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 { + if chanType.HasAnchors() { + return 1 + } + + return 0 +} + // CommitWeight returns the base commitment weight before adding HTLCs. func CommitWeight(chanType channeldb.ChannelType) int64 { // If this commitment has anchors, it will be slightly heavier. diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index c8f14a96..3c37cd8d 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -54,10 +54,13 @@ func createHtlcSuccessTx(chanType channeldb.ChannelType, successTx := wire.NewMsgTx(2) // The input to the transaction is the outpoint that creates the - // original HTLC on the sender's commitment transaction. - successTx.AddTxIn(&wire.TxIn{ + // original HTLC on the sender's commitment transaction. Set the + // sequence number based on the channel type. + txin := &wire.TxIn{ PreviousOutPoint: htlcOutput, - }) + Sequence: HtlcSecondLevelInputSequence(chanType), + } + successTx.AddTxIn(txin) // Next, we'll generate the script used as the output for all second // level HTLC which forces a covenant w.r.t what can be done with all @@ -110,10 +113,13 @@ func createHtlcTimeoutTx(chanType channeldb.ChannelType, timeoutTx.LockTime = cltvExpiry // The input to the transaction is the outpoint that creates the - // original HTLC on the sender's commitment transaction. - timeoutTx.AddTxIn(&wire.TxIn{ + // original HTLC on the sender's commitment transaction. Set the + // sequence number based on the channel type. + txin := &wire.TxIn{ PreviousOutPoint: htlcOutput, - }) + Sequence: HtlcSecondLevelInputSequence(chanType), + } + timeoutTx.AddTxIn(txin) // Next, we'll generate the script used as the output for all second // level HTLC which forces a covenant w.r.t what can be done with all From 92af2342daaf0cb4220c74794d05cfcf97f20c09 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:47 +0100 Subject: [PATCH 21/31] lnwallet+nursery+input: set sequence=1 for direct HTLC spends --- contractcourt/htlc_success_resolver.go | 1 + input/input.go | 13 +++++++------ lnwallet/channel.go | 9 ++++++--- utxonursery.go | 10 ++++++---- utxonursery_test.go | 3 ++- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index c13c52e8..38fa7fd0 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -118,6 +118,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { &h.htlcResolution.SweepSignDesc, h.htlcResolution.Preimage[:], h.broadcastHeight, + h.htlcResolution.CsvDelay, ) // With the input created, we can now generate the full diff --git a/input/input.go b/input/input.go index 7f5593ec..38a1651e 100644 --- a/input/input.go +++ b/input/input.go @@ -156,15 +156,16 @@ type HtlcSucceedInput struct { // MakeHtlcSucceedInput assembles a new redeem input that can be used to // construct a sweep transaction. func MakeHtlcSucceedInput(outpoint *wire.OutPoint, - signDescriptor *SignDescriptor, - preimage []byte, heightHint uint32) HtlcSucceedInput { + signDescriptor *SignDescriptor, preimage []byte, heightHint, + blocksToMaturity uint32) HtlcSucceedInput { return HtlcSucceedInput{ inputKit: inputKit{ - outpoint: *outpoint, - witnessType: HtlcAcceptedRemoteSuccess, - signDesc: *signDescriptor, - heightHint: heightHint, + outpoint: *outpoint, + witnessType: HtlcAcceptedRemoteSuccess, + signDesc: *signDescriptor, + heightHint: heightHint, + blockToMaturity: blocksToMaturity, }, preimage: preimage, } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index dbb2000a..a348254d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -5253,7 +5253,8 @@ type IncomingHtlcResolution struct { // pass after the SignedSuccessTx is confirmed in the chain before the // output can be swept. // - // NOTE: If SignedSuccessTx is nil, then this field isn't needed. + // NOTE: If SignedTimeoutTx is nil, then this field denotes the CSV + // delay needed to spend from the commitment transaction. CsvDelay uint32 // ClaimOutpoint is the final outpoint that needs to be spent in order @@ -5293,7 +5294,8 @@ type OutgoingHtlcResolution struct { // pass after the SignedTimeoutTx is confirmed in the chain before the // output can be swept. // - // NOTE: If SignedTimeoutTx is nil, then this field isn't needed. + // NOTE: If SignedTimeoutTx is nil, then this field denotes the CSV + // delay needed to spend from the commitment transaction. CsvDelay uint32 // ClaimOutpoint is the final outpoint that needs to be spent in order @@ -5366,6 +5368,7 @@ func newOutgoingHtlcResolution(signer input.Signer, }, HashType: txscript.SigHashAll, }, + CsvDelay: HtlcSecondLevelInputSequence(chanType), }, nil } @@ -5487,7 +5490,6 @@ func newIncomingHtlcResolution(signer input.Signer, // SignDescriptor needed to sweep the output. return &IncomingHtlcResolution{ ClaimOutpoint: op, - CsvDelay: csvDelay, SweepSignDesc: input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, @@ -5498,6 +5500,7 @@ func newIncomingHtlcResolution(signer input.Signer, }, HashType: txscript.SigHashAll, }, + CsvDelay: HtlcSecondLevelInputSequence(chanType), }, nil } diff --git a/utxonursery.go b/utxonursery.go index d05a04ed..abff3547 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -397,10 +397,11 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint, // Otherwise, this is actually a kid output as we can sweep it // once the commitment transaction confirms, and the absolute - // CLTV lock has expired. We set the CSV delay to zero to - // indicate this is actually a CLTV output. + // CLTV lock has expired. We set the CSV delay what the + // resolution encodes, since the sequence number must be set + // accordingly. htlcOutput := makeKidOutput( - &htlcRes.ClaimOutpoint, &chanPoint, 0, + &htlcRes.ClaimOutpoint, &chanPoint, htlcRes.CsvDelay, input.HtlcOfferedRemoteTimeout, &htlcRes.SweepSignDesc, htlcRes.Expiry, ) @@ -1271,7 +1272,8 @@ type kidOutput struct { // output. // // NOTE: This will be set for: commitment outputs, and incoming HTLC's. - // Otherwise, this will be zero. + // Otherwise, this will be zero. It will also be non-zero for + // commitment types which requires confirmed spends. blocksToMaturity uint32 // absoluteMaturity is the absolute height that this output will be diff --git a/utxonursery_test.go b/utxonursery_test.go index 579bddcf..cbc91269 100644 --- a/utxonursery_test.go +++ b/utxonursery_test.go @@ -603,7 +603,6 @@ func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution Value: 10000, }, }, - CsvDelay: 2, } if onLocalCommitment { @@ -620,8 +619,10 @@ func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution } outgoingRes.SignedTimeoutTx = timeoutTx + outgoingRes.CsvDelay = 2 } else { outgoingRes.ClaimOutpoint = htlcOp + outgoingRes.CsvDelay = 0 } return &outgoingRes From ad8e9f30c6a5c376cb049f2b5f849d91f6f38e4d Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:47 +0100 Subject: [PATCH 22/31] lnwallet+breacharbiter: record local csv delay --- breacharbiter.go | 14 ++++++++++++++ lnwallet/channel.go | 17 +++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/breacharbiter.go b/breacharbiter.go index 3706f3bd..eeee4f8e 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -896,6 +896,13 @@ func (bo *breachedOutput) CraftInputScript(signer input.Signer, txn *wire.MsgTx, // must be built on top of the confirmation height before the output can be // spent. func (bo *breachedOutput) BlocksToMaturity() uint32 { + // If the output is a to_remote output we can claim, and it's of the + // confirmed type, we must wait one block before claiming it. + if bo.witnessType == input.CommitmentToRemoteConfirmed { + return 1 + } + + // All other breached outputs have no CSV delay. return 0 } @@ -952,6 +959,12 @@ func newRetributionInfo(chanPoint *wire.OutPoint, witnessType = input.CommitSpendNoDelayTweakless } + // If the local delay is non-zero, it means this output is of + // the confirmed to_remote type. + if breachInfo.LocalDelay != 0 { + witnessType = input.CommitmentToRemoteConfirmed + } + localOutput := makeBreachedOutput( &breachInfo.LocalOutpoint, witnessType, @@ -1117,6 +1130,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64, for _, input := range inputs { txn.AddTxIn(&wire.TxIn{ PreviousOutPoint: *input.OutPoint(), + Sequence: input.BlocksToMaturity(), }) } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index a348254d..6b072ade 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2013,6 +2013,10 @@ type BreachRetribution struct { // party) within the breach transaction. LocalOutpoint wire.OutPoint + // LocalDelay is the CSV delay for the to_remote script on the breached + // commitment. + LocalDelay uint32 + // RemoteOutputSignDesc is a SignDescriptor which is capable of // generating the signature required to claim the funds as described // within the revocation clause of the remote party's commitment @@ -2026,6 +2030,10 @@ type BreachRetribution struct { // party within the breach transaction. RemoteOutpoint wire.OutPoint + // RemoteDelay specifies the CSV delay applied to to-local scripts on + // the breaching commitment transaction. + RemoteDelay uint32 + // HtlcRetributions is a slice of HTLC retributions for each output // active HTLC output within the breached commitment transaction. HtlcRetributions []HtlcRetribution @@ -2034,10 +2042,6 @@ type BreachRetribution struct { // breaching commitment transaction. This allows downstream clients to // have access to the public keys used in the scripts. KeyRing *CommitmentKeyRing - - // RemoteDelay specifies the CSV delay applied to to-local scripts on - // the breaching commitment transaction. - RemoteDelay uint32 } // NewBreachRetribution creates a new fully populated BreachRetribution for the @@ -2090,7 +2094,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // Since it is the remote breach we are reconstructing, the output going // to us will be a to-remote script with our local params. - ourScript, _, err := CommitScriptToRemote( + ourScript, ourDelay, err := CommitScriptToRemote( chanState.ChanType, keyRing.ToRemoteKey, ) if err != nil { @@ -2226,11 +2230,12 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, PendingHTLCs: revokedSnapshot.Htlcs, LocalOutpoint: ourOutpoint, LocalOutputSignDesc: ourSignDesc, + LocalDelay: ourDelay, RemoteOutpoint: theirOutpoint, RemoteOutputSignDesc: theirSignDesc, + RemoteDelay: theirDelay, HtlcRetributions: htlcRetributions, KeyRing: keyRing, - RemoteDelay: theirDelay, }, nil } From 8741b9372374f5a36867a7f327452961ee830d75 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 23/31] lnwallet/reservation: add non-initiator balance check If we are the initiator, we check that our starting balance after subtracting fees are not less than two times the default dust limit. This commit adds a similar check for the non-initiator case, checking that the remote party has a starting balance of reasonable size. --- lnwallet/reservation.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index b7b63da5..c371f6f0 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -213,6 +213,16 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, ) } + // Similarly we ensure their balance is reasonable if we are not the + // initiator. + if !initiator && theirBalance.ToSatoshis() <= 2*DefaultDustLimit() { + return nil, ErrFunderBalanceDust( + int64(commitFee), + int64(theirBalance.ToSatoshis()), + int64(2*DefaultDustLimit()), + ) + } + // Next we'll set the channel type based on what we can ascertain about // the balances/push amount within the channel. var chanType channeldb.ChannelType From f95a82bf5f76151af669ca594ca59e24304fd563 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 24/31] lnwallet+funding: create CommitmentType enum --- fundingmanager.go | 48 ++++++++++++++++++++++++-------------- lnwallet/interface_test.go | 22 ++++++++++------- lnwallet/reservation.go | 30 ++++++++++++++++++++++-- lnwallet/wallet.go | 8 +++---- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index 7913a11f..bf4ed31b 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -1106,6 +1106,28 @@ func (f *fundingManager) processFundingOpen(msg *lnwire.OpenChannel, } } +// commitmentType returns the commitment type to use for the channel, based on +// the features the two peers have available. +func commitmentType(localFeatures, + remoteFeatures *lnwire.FeatureVector) lnwallet.CommitmentType { + + localTweakless := localFeatures.HasFeature( + lnwire.StaticRemoteKeyOptional, + ) + remoteTweakless := remoteFeatures.HasFeature( + lnwire.StaticRemoteKeyOptional, + ) + + // If both nodes are signaling the proper feature bit for tweakless + // copmmitments, we'll use that. + if localTweakless && remoteTweakless { + return lnwallet.CommitmentTypeTweakless + } + + // Otherwise we'll fall back to the legacy type. + return lnwallet.CommitmentTypeLegacy +} + // handleFundingOpen creates an initial 'ChannelReservation' within the wallet, // then responds to the source peer with an accept channel message progressing // the funding workflow. @@ -1228,13 +1250,9 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { // negotiated the new tweakless commitment format. This is only the // case if *both* us and the remote peer are signaling the proper // feature bit. - localTweakless := fmsg.peer.LocalFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, + commitType := commitmentType( + fmsg.peer.LocalFeatures(), fmsg.peer.RemoteFeatures(), ) - remoteTweakless := fmsg.peer.RemoteFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, - ) - tweaklessCommitment := localTweakless && remoteTweakless chainHash := chainhash.Hash(msg.ChainHash) req := &lnwallet.InitFundingReserveMsg{ ChainHash: &chainHash, @@ -1248,7 +1266,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { PushMSat: msg.PushAmount, Flags: msg.ChannelFlags, MinConfs: 1, - Tweakless: tweaklessCommitment, + CommitType: commitType, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -1307,9 +1325,9 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { reservation.SetOurUpfrontShutdown(shutdown) fndgLog.Infof("Requiring %v confirmations for pendingChan(%x): "+ - "amt=%v, push_amt=%v, tweakless=%v, upfrontShutdown=%x", numConfsReq, + "amt=%v, push_amt=%v, committype=%v, upfrontShutdown=%x", numConfsReq, fmsg.msg.PendingChannelID, amt, msg.PushAmount, - tweaklessCommitment, msg.UpfrontShutdownScript) + commitType, msg.UpfrontShutdownScript) // Generate our required constraints for the remote party. remoteCsvDelay := f.cfg.RequiredRemoteDelay(amt) @@ -2904,13 +2922,9 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { // negotiated the new tweakless commitment format. This is only the // case if *both* us and the remote peer are signaling the proper // feature bit. - localTweakless := msg.peer.LocalFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, + commitType := commitmentType( + msg.peer.LocalFeatures(), msg.peer.RemoteFeatures(), ) - remoteTweakless := msg.peer.RemoteFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, - ) - tweaklessCommitment := localTweakless && remoteTweakless req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.chainHash, PendingChanID: chanID, @@ -2924,7 +2938,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { PushMSat: msg.pushAmt, Flags: channelFlags, MinConfs: msg.minConfs, - Tweakless: tweaklessCommitment, + CommitType: commitType, ChanFunder: msg.chanFunder, } @@ -3012,7 +3026,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { maxHtlcs := f.cfg.RequiredRemoteMaxHTLCs(capacity) fndgLog.Infof("Starting funding workflow with %v for pending_id(%x), "+ - "tweakless=%v", msg.peer.Address(), chanID, tweaklessCommitment) + "committype=%v", msg.peer.Address(), chanID, commitType) fundingOpen := lnwire.OpenChannel{ ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash, diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index db907d5a..6ebb7ff9 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -700,7 +700,8 @@ func testCancelNonExistentReservation(miner *rpctest.Harness, // Create our own reservation, give it some ID. res, err := lnwallet.NewChannelReservation( 10000, 10000, feePerKw, alice, 22, 10, &testHdSeed, - lnwire.FFAnnounceChannel, true, nil, [32]byte{}, + lnwire.FFAnnounceChannel, lnwallet.CommitmentTypeTweakless, + nil, [32]byte{}, ) if err != nil { t.Fatalf("unable to create res: %v", err) @@ -738,7 +739,7 @@ func testReservationInitiatorBalanceBelowDustCancel(miner *rpctest.Harness, FundingFeePerKw: 1000, PushMSat: 0, Flags: lnwire.FFAnnounceChannel, - Tweakless: true, + CommitType: lnwallet.CommitmentTypeTweakless, } _, err = alice.InitChannelReservation(req) switch { @@ -793,7 +794,8 @@ func assertContributionInitPopulated(t *testing.T, c *lnwallet.ChannelContributi } func testSingleFunderReservationWorkflow(miner *rpctest.Harness, - alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool, + alice, bob *lnwallet.LightningWallet, t *testing.T, + commitType lnwallet.CommitmentType, aliceChanFunder chanfunding.Assembler, fetchFundingTx func() *wire.MsgTx, pendingChanID [32]byte) { @@ -823,7 +825,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, FundingFeePerKw: feePerKw, PushMSat: pushAmt, Flags: lnwire.FFAnnounceChannel, - Tweakless: tweakless, + CommitType: commitType, ChanFunder: aliceChanFunder, } aliceChanReservation, err := alice.InitChannelReservation(aliceReq) @@ -874,7 +876,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, FundingFeePerKw: feePerKw, PushMSat: pushAmt, Flags: lnwire.FFAnnounceChannel, - Tweakless: tweakless, + CommitType: commitType, } bobChanReservation, err := bob.InitChannelReservation(bobReq) if err != nil { @@ -2543,7 +2545,8 @@ var walletTests = []walletTestCase{ bob *lnwallet.LightningWallet, t *testing.T) { testSingleFunderReservationWorkflow( - miner, alice, bob, t, false, nil, nil, + miner, alice, bob, t, + lnwallet.CommitmentTypeLegacy, nil, nil, [32]byte{}, ) }, @@ -2554,7 +2557,8 @@ var walletTests = []walletTestCase{ bob *lnwallet.LightningWallet, t *testing.T) { testSingleFunderReservationWorkflow( - miner, alice, bob, t, true, nil, nil, + miner, alice, bob, t, + lnwallet.CommitmentTypeTweakless, nil, nil, [32]byte{}, ) }, @@ -2809,8 +2813,8 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness, // pending channel ID generated above to allow Alice and Bob to track // the funding flow externally. testSingleFunderReservationWorkflow( - miner, alice, bob, t, true, aliceExternalFunder, - func() *wire.MsgTx { + miner, alice, bob, t, lnwallet.CommitmentTypeTweakless, + aliceExternalFunder, func() *wire.MsgTx { return fundingTx }, pendingChanID, ) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index c371f6f0..ba4c631e 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -15,6 +15,32 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) +// CommitmentType is an enum indicating the commitment type we should use for +// the channel we are opening. +type CommitmentType int + +const ( + // CommitmentTypeLegacy is the legacy commitment format with a tweaked + // to_remote key. + CommitmentTypeLegacy = iota + + // CommitmentTypeTweakless is a newer commitment format where the + // to_remote key is static. + CommitmentTypeTweakless +) + +// String returns the name of the CommitmentType. +func (c CommitmentType) String() string { + switch c { + case CommitmentTypeLegacy: + return "legacy" + case CommitmentTypeTweakless: + return "tweakless" + default: + return "invalid" + } +} + // ChannelContribution is the primary constituent of the funding workflow // within lnwallet. Each side first exchanges their respective contributions // along with channel specific parameters like the min fee/KB. Once @@ -136,7 +162,7 @@ type ChannelReservation struct { func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet, id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash, - flags lnwire.FundingFlag, tweaklessCommit bool, + flags lnwire.FundingFlag, commitType CommitmentType, fundingAssembler chanfunding.Assembler, pendingChanID [32]byte) (*ChannelReservation, error) { @@ -231,7 +257,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // non-zero push amt (there's no pushing for dual funder), then this is // a single-funder channel. if ourBalance == 0 || theirBalance == 0 || pushMSat != 0 { - if tweaklessCommit { + if commitType == CommitmentTypeTweakless { chanType |= channeldb.SingleFunderTweaklessBit } else { chanType |= channeldb.SingleFunderBit diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 5649b652..73eaf217 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -98,9 +98,9 @@ type InitFundingReserveMsg struct { // output selected to fund the channel should satisfy. MinConfs int32 - // Tweakless indicates if the channel should use the new tweakless - // commitment format or not. - Tweakless bool + // CommitType indicates what type of commitment type the channel should + // be using, like tweakless or anchors. + CommitType CommitmentType // ChanFunder is an optional channel funder that allows the caller to // control exactly how the channel funding is carried out. If not @@ -568,7 +568,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg reservation, err := NewChannelReservation( capacity, localFundingAmt, req.CommitFeePerKw, l, id, req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags, - req.Tweakless, req.ChanFunder, req.PendingChanID, + req.CommitType, req.ChanFunder, req.PendingChanID, ) if err != nil { if fundingIntent != nil { From 51c5352ae4b0b4e4d1209f2c4812e61f6d66bb92 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 25/31] itest: make commit type enum To prepare for adding more commit types to test for basic channel funding, we make the commit type an enum that gets its own set of subtests. --- lntest/itest/lnd_test.go | 66 ++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 665426a3..fe305903 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -953,6 +953,43 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { restoreCheckBalance(6*btcutil.SatoshiPerBitcoin, 6, 20, nil) } +// commitType is a simple enum used to run though the basic funding flow with +// different commitment formats. +type commitType byte + +const ( + // commitTypeLegacy is the old school commitment type. + commitTypeLegacy = iota + + // commiTypeTweakless is the commitment type where the remote key is + // static (non-tweaked). + commitTypeTweakless +) + +// String returns that name of the commitment type. +func (c commitType) String() string { + switch c { + case commitTypeLegacy: + return "legacy" + case commitTypeTweakless: + return "tweakless" + default: + return "invalid" + } +} + +// Args returns the command line flag to supply to enable this commitment type. +func (c commitType) Args() []string { + switch c { + case commitTypeLegacy: + return []string{"--legacyprotocol.committweak"} + case commitTypeTweakless: + return []string{} + } + + return nil +} + // basicChannelFundingTest is a sub-test of the main testBasicChannelFunding // test. Given two nodes: Alice and Bob, it'll assert proper channel creation, // then return a function closure that should be called to assert proper @@ -1049,19 +1086,23 @@ func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() + // Run through the test with combinations of all the different + // commitment types. + allTypes := []commitType{ + commitTypeLegacy, + commitTypeTweakless, + } + test: // We'll test all possible combinations of the feature bit presence // that both nodes can signal for this new channel type. We'll make a // new Carol+Dave for each test instance as well. - for _, carolTweakless := range []bool{true, false} { - for _, daveTweakless := range []bool{true, false} { + for _, carolCommitType := range allTypes { + for _, daveCommitType := range allTypes { // Based on the current tweak variable for Carol, we'll // preferentially signal the legacy commitment format. // We do the same for Dave shortly below. - var carolArgs []string - if !carolTweakless { - carolArgs = []string{"--legacyprotocol.committweak"} - } + carolArgs := carolCommitType.Args() carol, err := net.NewNode("Carol", carolArgs) if err != nil { t.Fatalf("unable to create new node: %v", err) @@ -1075,10 +1116,7 @@ test: t.Fatalf("unable to send coins to carol: %v", err) } - var daveArgs []string - if !daveTweakless { - daveArgs = []string{"--legacyprotocol.committweak"} - } + daveArgs := daveCommitType.Args() dave, err := net.NewNode("Dave", daveArgs) if err != nil { t.Fatalf("unable to create new node: %v", err) @@ -1093,8 +1131,8 @@ test: t.Fatalf("unable to connect peers: %v", err) } - testName := fmt.Sprintf("carol_tweak=%v,dave_tweak=%v", - carolTweakless, daveTweakless) + testName := fmt.Sprintf("carol_commit=%v,dave_commit=%v", + carolCommitType, daveCommitType) ht := t success := t.t.Run(testName, func(t *testing.T) { @@ -1105,6 +1143,10 @@ test: t.Fatalf("failed funding flow: %v", err) } + carolTweakless := carolCommitType == commitTypeTweakless + + daveTweakless := daveCommitType == commitTypeTweakless + tweaklessSignalled := carolTweakless && daveTweakless tweaklessChans := (carolChannel.StaticRemoteKey && daveChannel.StaticRemoteKey) From 44756b5811059d65076fbe2eb4cdea8596f73375 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 26/31] cfg: rename legacyprotocol to protocol Since we are also going to use it for experimental new features. --- config.go | 2 +- lncfg/protocol_legacy_off.go | 14 +++++++------- lncfg/protocol_legacy_on.go | 24 ++++++++++++------------ lntest/itest/lnd_test.go | 4 ++-- server.go | 8 ++++---- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/config.go b/config.go index fd5ee8b6..5308b58c 100644 --- a/config.go +++ b/config.go @@ -345,7 +345,7 @@ type config struct { Watchtower *lncfg.Watchtower `group:"watchtower" namespace:"watchtower"` - LegacyProtocol *lncfg.LegacyProtocol `group:"legacyprotocol" namespace:"legacyprotocol"` + ProtocolOptions *lncfg.ProtocolOptions `group:"protocol" namespace:"protocol"` AllowCircularRoute bool `long:"allow-circular-route" description:"If true, our node will allow htlc forwards that arrive and depart on the same channel."` } diff --git a/lncfg/protocol_legacy_off.go b/lncfg/protocol_legacy_off.go index 7e1e47d3..0011bb7f 100644 --- a/lncfg/protocol_legacy_off.go +++ b/lncfg/protocol_legacy_off.go @@ -2,21 +2,21 @@ package lncfg -// LegacyProtocol is a struct that we use to be able to test backwards +// ProtocolOptions is a struct that we use to be able to test backwards // compatibility of protocol additions, while defaulting to the latest within -// lnd. -type LegacyProtocol struct { +// lnd, or to enable experimental protocol changes. +type ProtocolOptions struct { } // LegacyOnion returns true if the old legacy onion format should be used when // we're an intermediate or final hop. This controls if we set the // TLVOnionPayloadOptional bit or not. -func (l *LegacyProtocol) LegacyOnion() bool { +func (l *ProtocolOptions) LegacyOnion() bool { return false } -// LegacyOnion returns true if the old commitment format should be used for new -// funded channels. -func (l *LegacyProtocol) LegacyCommitment() bool { +// LegacyCommitment returns true if the old commitment format should be used +// for new funded channels. +func (l *ProtocolOptions) LegacyCommitment() bool { return false } diff --git a/lncfg/protocol_legacy_on.go b/lncfg/protocol_legacy_on.go index d384adc8..e091c6a6 100644 --- a/lncfg/protocol_legacy_on.go +++ b/lncfg/protocol_legacy_on.go @@ -2,14 +2,14 @@ package lncfg -// LegacyProtocol is a struct that we use to be able to test backwards +// ProtocolOptions is a struct that we use to be able to test backwards // compatibility of protocol additions, while defaulting to the latest within -// lnd. -type LegacyProtocol struct { - // Onion if set to true, then we won't signal TLVOnionPayloadOptional. - // As a result, nodes that include us in the route won't use the new - // modern onion framing. - Onion bool `long:"onion" description:"force node to not advertise the new modern TLV onion format"` +// lnd, or to enable experimental protocol changes. +type ProtocolOptions struct { + // LegacyOnionFormat if set to true, then we won't signal + // TLVOnionPayloadOptional. As a result, nodes that include us in the + // route won't use the new modern onion framing. + LegacyOnionFormat bool `long:"legacyonion" description:"force node to not advertise the new modern TLV onion format"` // CommitmentTweak guards if we should use the old legacy commitment // protocol, or the newer variant that doesn't have a tweak for the @@ -21,12 +21,12 @@ type LegacyProtocol struct { // LegacyOnion returns true if the old legacy onion format should be used when // we're an intermediate or final hop. This controls if we set the // TLVOnionPayloadOptional bit or not. -func (l *LegacyProtocol) LegacyOnion() bool { - return l.Onion +func (l *ProtocolOptions) LegacyOnion() bool { + return l.LegacyOnionFormat } -// LegacyOnion returns true if the old commitment format should be used for new -// funded channels. -func (l *LegacyProtocol) LegacyCommitment() bool { +// LegacyCommitment returns true if the old commitment format should be used +// for new funded channels. +func (l *ProtocolOptions) LegacyCommitment() bool { return l.CommitmentTweak } diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index fe305903..93358d10 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -982,7 +982,7 @@ func (c commitType) String() string { func (c commitType) Args() []string { switch c { case commitTypeLegacy: - return []string{"--legacyprotocol.committweak"} + return []string{"--protocol.committweak"} case commitTypeTweakless: return []string{} } @@ -4247,7 +4247,7 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { // // First, we'll create Dave and establish a channel to Alice. Dave will // be running an older node that requires the legacy onion payload. - daveArgs := []string{"--legacyprotocol.onion"} + daveArgs := []string{"--protocol.legacyonion"} dave, err := net.NewNode("Dave", daveArgs) if err != nil { t.Fatalf("unable to create new nodes: %v", err) diff --git a/server.go b/server.go index bde65671..a4bff136 100644 --- a/server.go +++ b/server.go @@ -336,13 +336,13 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, // Only if we're not being forced to use the legacy onion format, will // we signal our knowledge of the new TLV onion format. - if !cfg.LegacyProtocol.LegacyOnion() { + if !cfg.ProtocolOptions.LegacyOnion() { globalFeatures.Set(lnwire.TLVOnionPayloadOptional) } // Similarly, we default to the new modern commitment format unless the // legacy commitment config is set to true. - if !cfg.LegacyProtocol.LegacyCommitment() { + if !cfg.ProtocolOptions.LegacyCommitment() { globalFeatures.Set(lnwire.StaticRemoteKeyOptional) } @@ -375,8 +375,8 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, ) featureMgr, err := feature.NewManager(feature.Config{ - NoTLVOnion: cfg.LegacyProtocol.LegacyOnion(), - NoStaticRemoteKey: cfg.LegacyProtocol.LegacyCommitment(), + NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(), + NoStaticRemoteKey: cfg.ProtocolOptions.LegacyCommitment(), }) if err != nil { return nil, err From 21126ab0f301cf6252c0608545e49c325ce8dded Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 27/31] multi: optionally enable and signal anchor support Defaults to disabled. --- feature/default_sets.go | 4 ++++ feature/deps.go | 3 +++ feature/manager.go | 7 +++++++ lncfg/protocol_legacy_off.go | 12 +++++++++--- lncfg/protocol_legacy_on.go | 16 +++++++++++++--- lnwire/features.go | 12 ++++++++++++ server.go | 16 ++++++++++++---- 7 files changed, 60 insertions(+), 10 deletions(-) diff --git a/feature/default_sets.go b/feature/default_sets.go index 32807654..8054894e 100644 --- a/feature/default_sets.go +++ b/feature/default_sets.go @@ -43,4 +43,8 @@ var defaultSetDesc = setDesc{ SetNodeAnn: {}, // N SetInvoice: {}, // 9 }, + lnwire.AnchorsOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, } diff --git a/feature/deps.go b/feature/deps.go index 343f3f60..c2b8f175 100644 --- a/feature/deps.go +++ b/feature/deps.go @@ -55,6 +55,9 @@ var deps = depDesc{ lnwire.MPPOptional: { lnwire.PaymentAddrOptional: {}, }, + lnwire.AnchorsOptional: { + lnwire.StaticRemoteKeyOptional: {}, + }, } // ValidateDeps asserts that a feature vector sets all features and their diff --git a/feature/manager.go b/feature/manager.go index 01c82123..159540c2 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -17,6 +17,9 @@ type Config struct { // NoStaticRemoteKey unsets any optional or required StaticRemoteKey // bits from all feature sets. NoStaticRemoteKey bool + + // NoAnchors unsets any bits signaling support for anchor outputs. + NoAnchors bool } // Manager is responsible for generating feature vectors for different requested @@ -76,6 +79,10 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { raw.Unset(lnwire.StaticRemoteKeyOptional) raw.Unset(lnwire.StaticRemoteKeyRequired) } + if cfg.NoAnchors { + raw.Unset(lnwire.AnchorsOptional) + raw.Unset(lnwire.AnchorsRequired) + } // Ensure that all of our feature sets properly set any // dependent features. diff --git a/lncfg/protocol_legacy_off.go b/lncfg/protocol_legacy_off.go index 0011bb7f..bd589c24 100644 --- a/lncfg/protocol_legacy_off.go +++ b/lncfg/protocol_legacy_off.go @@ -15,8 +15,14 @@ func (l *ProtocolOptions) LegacyOnion() bool { return false } -// LegacyCommitment returns true if the old commitment format should be used -// for new funded channels. -func (l *ProtocolOptions) LegacyCommitment() bool { +// NoStaticRemoteKey returns true if the old commitment format with a tweaked +// remote key should be used for new funded channels. +func (l *ProtocolOptions) NoStaticRemoteKey() bool { + return false +} + +// AnchorCommitments returns true if support for the the anchor commitment type +// should be signaled. +func (l *ProtocolOptions) AnchorCommitments() bool { return false } diff --git a/lncfg/protocol_legacy_on.go b/lncfg/protocol_legacy_on.go index e091c6a6..9be0d1d6 100644 --- a/lncfg/protocol_legacy_on.go +++ b/lncfg/protocol_legacy_on.go @@ -16,6 +16,10 @@ type ProtocolOptions struct { // remote party's output in the commitment. If set to true, then we // won't signal StaticRemoteKeyOptional. CommitmentTweak bool `long:"committweak" description:"force node to not advertise the new commitment format"` + + // Anchors should be set if we want to support opening or accepting + // channels having the anchor commitment type. + Anchors bool `long:"anchors" description:"EXPERIMENTAL: enable experimental support for anchor commitments. Won't work with watchtowers or static channel backups"` } // LegacyOnion returns true if the old legacy onion format should be used when @@ -25,8 +29,14 @@ func (l *ProtocolOptions) LegacyOnion() bool { return l.LegacyOnionFormat } -// LegacyCommitment returns true if the old commitment format should be used -// for new funded channels. -func (l *ProtocolOptions) LegacyCommitment() bool { +// NoStaticRemoteKey returns true if the old commitment format with a tweaked +// remote key should be used for new funded channels. +func (l *ProtocolOptions) NoStaticRemoteKey() bool { return l.CommitmentTweak } + +// AnchorCommitments returns true if support for the the anchor commitment type +// should be signaled. +func (l *ProtocolOptions) AnchorCommitments() bool { + return l.Anchors +} diff --git a/lnwire/features.go b/lnwire/features.go index db170f6d..4e5899c2 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -101,6 +101,16 @@ const ( // HTLC. MPPOptional FeatureBit = 17 + // AnchorsRequired is a required feature bit that signals that the node + // requires channels to be made using commitments having anchor + // outputs. + AnchorsRequired FeatureBit = 1336 + + // AnchorsRequired is an optional feature bit that signals that the + // node supports channels to be made using commitments having anchor + // outputs. + AnchorsOptional FeatureBit = 1337 + // maxAllowedSize is a maximum allowed size of feature vector. // // NOTE: Within the protocol, the maximum allowed message size is 65535 @@ -138,6 +148,8 @@ var Features = map[FeatureBit]string{ PaymentAddrRequired: "payment-addr", MPPOptional: "multi-path-payments", MPPRequired: "multi-path-payments", + AnchorsRequired: "anchor-commitments", + AnchorsOptional: "anchor-commitments", } // RawFeatureVector represents a set of feature bits as defined in BOLT-09. A diff --git a/server.go b/server.go index a4bff136..103c9aad 100644 --- a/server.go +++ b/server.go @@ -340,12 +340,19 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, globalFeatures.Set(lnwire.TLVOnionPayloadOptional) } - // Similarly, we default to the new modern commitment format unless the - // legacy commitment config is set to true. - if !cfg.ProtocolOptions.LegacyCommitment() { + // Similarly, we default to supporting the new modern commitment format + // where the remote key is static unless the protocol config is set to + // keep using the older format. + if !cfg.ProtocolOptions.NoStaticRemoteKey() { globalFeatures.Set(lnwire.StaticRemoteKeyOptional) } + // We only signal that we support the experimental anchor commitments + // if explicitly enabled in the config. + if cfg.ProtocolOptions.AnchorCommitments() { + globalFeatures.Set(lnwire.AnchorsOptional) + } + var serializedPubKey [33]byte copy(serializedPubKey[:], privKey.PubKey().SerializeCompressed()) @@ -376,7 +383,8 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, featureMgr, err := feature.NewManager(feature.Config{ NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(), - NoStaticRemoteKey: cfg.ProtocolOptions.LegacyCommitment(), + NoStaticRemoteKey: cfg.ProtocolOptions.NoStaticRemoteKey(), + NoAnchors: !cfg.ProtocolOptions.AnchorCommitments(), }) if err != nil { return nil, err From fd93c568ea22cdc6b51a56f384f4ee7fab2c2284 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 28/31] config+link: disable watchtower for anchors --- htlcswitch/link.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index cd2d1fca..7c834827 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -419,7 +419,12 @@ func (l *channelLink) Start() error { // If the config supplied watchtower client, ensure the channel is // registered before trying to use it during operation. - if l.cfg.TowerClient != nil { + // TODO(halseth): support anchor types for watchtower. + state := l.channel.State() + if l.cfg.TowerClient != nil && state.ChanType.HasAnchors() { + l.log.Warnf("Skipping tower registration for anchor " + + "channel type") + } else if l.cfg.TowerClient != nil && !state.ChanType.HasAnchors() { err := l.cfg.TowerClient.RegisterChannel(l.ChanID()) if err != nil { return err @@ -1883,8 +1888,12 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // If we have a tower client, we'll proceed in backing up the // state that was just revoked. - if l.cfg.TowerClient != nil { - state := l.channel.State() + // TODO(halseth): support anchor types for watchtower. + state := l.channel.State() + if l.cfg.TowerClient != nil && state.ChanType.HasAnchors() { + l.log.Warnf("Skipping tower backup for anchor " + + "channel type") + } else if l.cfg.TowerClient != nil && !state.ChanType.HasAnchors() { breachInfo, err := lnwallet.NewBreachRetribution( state, state.RemoteCommitment.CommitHeight-1, 0, ) From 7adb1bcbaa9689e08e1e281d8cf65379adefc589 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:48 +0100 Subject: [PATCH 29/31] chanbackup: disable channel backup for anchor types --- chanbackup/backup.go | 17 ++++++++++++++--- chanbackup/pubsub.go | 6 ++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/chanbackup/backup.go b/chanbackup/backup.go index ca3698a5..406cc1bf 100644 --- a/chanbackup/backup.go +++ b/chanbackup/backup.go @@ -62,6 +62,12 @@ func FetchBackupForChan(chanPoint wire.OutPoint, return nil, fmt.Errorf("unable to find target channel") } + // TODO(halseth): support chan backups for anchor types. + if targetChan.ChanType.HasAnchors() { + return nil, fmt.Errorf("channel type does not support " + + "backups yet") + } + // Once we have the target channel, we can assemble the backup using // the source to obtain any extra information that we may need. staticChanBackup, err := assembleChanBackup(chanSource, targetChan) @@ -85,14 +91,19 @@ func FetchStaticChanBackups(chanSource LiveChannelSource) ([]Single, error) { // Now that we have all the channels, we'll use the chanSource to // obtain any auxiliary information we need to craft a backup for each // channel. - staticChanBackups := make([]Single, len(openChans)) - for i, openChan := range openChans { + staticChanBackups := make([]Single, 0, len(openChans)) + for _, openChan := range openChans { + // TODO(halseth): support chan backups for anchor types. + if openChan.ChanType.HasAnchors() { + continue + } + chanBackup, err := assembleChanBackup(chanSource, openChan) if err != nil { return nil, err } - staticChanBackups[i] = *chanBackup + staticChanBackups = append(staticChanBackups, *chanBackup) } return staticChanBackups, nil diff --git a/chanbackup/pubsub.go b/chanbackup/pubsub.go index b9331820..2bc74898 100644 --- a/chanbackup/pubsub.go +++ b/chanbackup/pubsub.go @@ -213,6 +213,12 @@ func (s *SubSwapper) backupUpdater() { // For all new open channels, we'll create a new SCB // given the required information. for _, newChan := range chanUpdate.NewChans { + // TODO(halseth): support chan backups for + // anchor types. + if newChan.ChanType.HasAnchors() { + continue + } + log.Debugf("Adding channel %v to backup state", newChan.FundingOutpoint) From ea2a58e80f61ccfd28f2b834e022a7a410c1b247 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:49 +0100 Subject: [PATCH 30/31] fundingmanager+lnwallet: enable anchor commitments If both nodes are signalling the feature, make all opened channels using this type. --- fundingmanager.go | 14 ++++++++++++++ lnwallet/reservation.go | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index bf4ed31b..3e28f2a8 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -1111,6 +1111,20 @@ func (f *fundingManager) processFundingOpen(msg *lnwire.OpenChannel, func commitmentType(localFeatures, remoteFeatures *lnwire.FeatureVector) lnwallet.CommitmentType { + // If both peers are signalling support for anchor commitments, this + // implicitly mean we'll create the channel of this type. Note that + // this also enables tweakless commitments, as anchor commitments are + // always tweakless. + localAnchors := localFeatures.HasFeature( + lnwire.AnchorsOptional, + ) + remoteAnchors := remoteFeatures.HasFeature( + lnwire.AnchorsOptional, + ) + if localAnchors && remoteAnchors { + return lnwallet.CommitmentTypeAnchors + } + localTweakless := localFeatures.HasFeature( lnwire.StaticRemoteKeyOptional, ) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index ba4c631e..823aec4a 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -27,6 +27,11 @@ const ( // CommitmentTypeTweakless is a newer commitment format where the // to_remote key is static. CommitmentTypeTweakless + + // CommitmentTypeAnchors is a commitment type that is tweakless, and + // has extra anchor ouputs in order to bump the fee of the commitment + // transaction. + CommitmentTypeAnchors ) // String returns the name of the CommitmentType. @@ -36,6 +41,8 @@ func (c CommitmentType) String() string { return "legacy" case CommitmentTypeTweakless: return "tweakless" + case CommitmentTypeAnchors: + return "anchors" default: return "invalid" } @@ -172,12 +179,25 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, initiator bool ) - commitFee := commitFeePerKw.FeeForWeight(input.CommitWeight) + // Based on the channel type, we determine the initial commit weight + // and fee. + commitWeight := int64(input.CommitWeight) + if commitType == CommitmentTypeAnchors { + commitWeight = input.AnchorCommitWeight + } + commitFee := commitFeePerKw.FeeForWeight(commitWeight) + localFundingMSat := lnwire.NewMSatFromSatoshis(localFundingAmt) // TODO(halseth): make method take remote funding amount directly // instead of inferring it from capacity and local amt. capacityMSat := lnwire.NewMSatFromSatoshis(capacity) + + // The total fee paid by the initiator will be the commitment fee in + // addition to the two anchor outputs. feeMSat := lnwire.NewMSatFromSatoshis(commitFee) + if commitType == CommitmentTypeAnchors { + feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize) + } // If we're the responder to a single-funder reservation, then we have // no initial balance in the channel unless the remote party is pushing @@ -257,7 +277,11 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // non-zero push amt (there's no pushing for dual funder), then this is // a single-funder channel. if ourBalance == 0 || theirBalance == 0 || pushMSat != 0 { - if commitType == CommitmentTypeTweakless { + // Both the tweakless type and the anchor type is tweakless, + // hence set the bit. + if commitType == CommitmentTypeTweakless || + commitType == CommitmentTypeAnchors { + chanType |= channeldb.SingleFunderTweaklessBit } else { chanType |= channeldb.SingleFunderBit @@ -277,6 +301,11 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.DualFunderBit } + // We are adding anchor outputs to our commitment. + if commitType == CommitmentTypeAnchors { + chanType |= channeldb.AnchorOutputsBit + } + return &ChannelReservation{ ourContribution: &ChannelContribution{ FundingAmount: ourBalance.ToSatoshis(), From b7885dbbae7cf36e1d00fbaf62084f74529bbb11 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 6 Mar 2020 16:11:49 +0100 Subject: [PATCH 31/31] lnwallet+size: select HTLC fees based on channel type --- input/size.go | 17 +++++++ lnwallet/channel.go | 102 +++++++++++++++++++++------------------ lnwallet/channel_test.go | 21 +++++--- lnwallet/commitment.go | 49 +++++++++++++++---- 4 files changed, 125 insertions(+), 64 deletions(-) diff --git a/input/size.go b/input/size.go index 005c2273..d5f5e2b0 100644 --- a/input/size.go +++ b/input/size.go @@ -202,6 +202,23 @@ const ( // which will transition an incoming HTLC to the delay-and-claim state. HtlcSuccessWeight = 703 + // HtlcConfirmedScriptOverhead is the extra length of an HTLC script + // that requires confirmation before it can be spent. These extra bytes + // is a result of the extra CSV check. + HtlcConfirmedScriptOverhead = 3 + + // HtlcTimeoutWeightConfirmed is the weight of the HTLC timeout + // transaction which will transition an outgoing HTLC to the + // delay-and-claim state, for the confirmed HTLC outputs. It is 3 bytes + // larger because of the additional CSV check in the input script. + HtlcTimeoutWeightConfirmed = HtlcTimeoutWeight + HtlcConfirmedScriptOverhead + + // HtlcSuccessWeightCOnfirmed is the weight of the HTLC success + // transaction which will transition an incoming HTLC to the + // delay-and-claim state, for the confirmed HTLC outputs. It is 3 bytes + // larger because of the cdditional CSV check in the input script. + HtlcSuccessWeightConfirmed = HtlcSuccessWeight + HtlcConfirmedScriptOverhead + // MaxHTLCNumber is the maximum number HTLCs which can be included in a // commitment transaction. This limit was chosen such that, in the case // of a contract breach, the punishment transaction is able to sweep diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 6b072ade..aca93b99 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -596,7 +596,7 @@ func locateOutputIndex(p *PaymentDescriptor, tx *wire.MsgTx, ourCommit bool, // we need to keep track of the indexes of each HTLC in order to properly write // the current state to disk, and also to locate the PaymentDescriptor // corresponding to HTLC outputs in the commitment transaction. -func (c *commitment) populateHtlcIndexes() error { +func (c *commitment) populateHtlcIndexes(chanType channeldb.ChannelType) error { // First, we'll set up some state to allow us to locate the output // index of the all the HTLC's within the commitment transaction. We // must keep this index so we can validate the HTLC signatures sent to @@ -608,8 +608,10 @@ func (c *commitment) populateHtlcIndexes() error { // populateIndex is a helper function that populates the necessary // indexes within the commitment view for a particular HTLC. populateIndex := func(htlc *PaymentDescriptor, incoming bool) error { - isDust := htlcIsDust(incoming, c.isOurs, c.feePerKw, - htlc.Amount.ToSatoshis(), c.dustLimit) + isDust := htlcIsDust( + chanType, incoming, c.isOurs, c.feePerKw, + htlc.Amount.ToSatoshis(), c.dustLimit, + ) var err error switch { @@ -782,8 +784,10 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, // generate them in order to locate the outputs within the commitment // transaction. As we'll mark dust with a special output index in the // on-disk state snapshot. - isDustLocal := htlcIsDust(htlc.Incoming, true, feeRate, - htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit) + isDustLocal := htlcIsDust( + chanType, htlc.Incoming, true, feeRate, + htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit, + ) if !isDustLocal && localCommitKeys != nil { ourP2WSH, ourWitnessScript, err = genHtlcScript( chanType, htlc.Incoming, true, htlc.RefundTimeout, @@ -793,8 +797,10 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, return pd, err } } - isDustRemote := htlcIsDust(htlc.Incoming, false, feeRate, - htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit) + isDustRemote := htlcIsDust( + chanType, htlc.Incoming, false, feeRate, + htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit, + ) if !isDustRemote && remoteCommitKeys != nil { theirP2WSH, theirWitnessScript, err = genHtlcScript( chanType, htlc.Incoming, false, htlc.RefundTimeout, @@ -930,7 +936,8 @@ func (lc *LightningChannel) diskCommitToMemCommit(isLocal bool, // Finally, we'll re-populate the HTLC index for this state so we can // properly locate each HTLC within the commitment transaction. - if err := commit.populateHtlcIndexes(); err != nil { + err = commit.populateHtlcIndexes(lc.channelState.ChanType) + if err != nil { return nil, err } @@ -1410,8 +1417,10 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob)) copy(pd.OnionBlob[:], wireMsg.OnionBlob[:]) - isDustRemote := htlcIsDust(false, false, feeRate, - wireMsg.Amount.ToSatoshis(), remoteDustLimit) + isDustRemote := htlcIsDust( + lc.channelState.ChanType, false, false, feeRate, + wireMsg.Amount.ToSatoshis(), remoteDustLimit, + ) if !isDustRemote { theirP2WSH, theirWitnessScript, err := genHtlcScript( lc.channelState.ChanType, false, false, @@ -2168,7 +2177,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // If the HTLC is dust, then we'll skip it as it doesn't have // an output on the commitment transaction. if htlcIsDust( - htlc.Incoming, false, + chanState.ChanType, htlc.Incoming, false, chainfee.SatPerKWeight(revokedSnapshot.FeePerKw), htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit, ) { @@ -2239,25 +2248,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, }, nil } -// htlcTimeoutFee returns the fee in satoshis required for an HTLC timeout -// transaction based on the current fee rate. -func htlcTimeoutFee(feePerKw chainfee.SatPerKWeight) btcutil.Amount { - return feePerKw.FeeForWeight(input.HtlcTimeoutWeight) -} - -// htlcSuccessFee returns the fee in satoshis required for an HTLC success -// transaction based on the current fee rate. -func htlcSuccessFee(feePerKw chainfee.SatPerKWeight) btcutil.Amount { - return feePerKw.FeeForWeight(input.HtlcSuccessWeight) -} - // htlcIsDust determines if an HTLC output is dust or not depending on two // bits: if the HTLC is incoming and if the HTLC will be placed on our // commitment transaction, or theirs. These two pieces of information are // require as we currently used second-level HTLC transactions as off-chain // covenants. Depending on the two bits, we'll either be using a timeout or // success transaction which have different weights. -func htlcIsDust(incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, +func htlcIsDust(chanType channeldb.ChannelType, + incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, htlcAmt, dustLimit btcutil.Amount) bool { // First we'll determine the fee required for this HTLC based on if this is @@ -2269,25 +2267,25 @@ func htlcIsDust(incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, // If this is an incoming HTLC on our commitment transaction, then the // second-level transaction will be a success transaction. case incoming && ourCommit: - htlcFee = htlcSuccessFee(feePerKw) + htlcFee = HtlcSuccessFee(chanType, feePerKw) // If this is an incoming HTLC on their commitment transaction, then // we'll be using a second-level timeout transaction as they've added // this HTLC. case incoming && !ourCommit: - htlcFee = htlcTimeoutFee(feePerKw) + htlcFee = HtlcTimeoutFee(chanType, feePerKw) // If this is an outgoing HTLC on our commitment transaction, then // we'll be using a timeout transaction as we're the sender of the // HTLC. case !incoming && ourCommit: - htlcFee = htlcTimeoutFee(feePerKw) + htlcFee = HtlcTimeoutFee(chanType, feePerKw) // If this is an outgoing HTLC on their commitment transaction, then // we'll be using an HTLC success transaction as they're the receiver // of this HTLC. case !incoming && !ourCommit: - htlcFee = htlcSuccessFee(feePerKw) + htlcFee = HtlcSuccessFee(chanType, feePerKw) } return (htlcAmt - htlcFee) < dustLimit @@ -2431,7 +2429,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // Finally, we'll populate all the HTLC indexes so we can track the // locations of each HTLC in the commitment state. - if err := c.populateHtlcIndexes(); err != nil { + if err := c.populateHtlcIndexes(lc.channelState.ChanType); err != nil { return nil, err } @@ -2769,8 +2767,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // dust output after taking into account second-level HTLC fees, then a // sigJob will be generated and appended to the current batch. for _, htlc := range remoteCommitView.incomingHTLCs { - if htlcIsDust(true, false, feePerKw, htlc.Amount.ToSatoshis(), - dustLimit) { + if htlcIsDust( + chanType, true, false, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -2785,7 +2785,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // HTLC timeout transaction for them. The output of the timeout // transaction needs to account for fees, so we'll compute the // required fee and output now. - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee // With the fee calculate, we can properly create the HTLC @@ -2822,8 +2822,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, sigBatch = append(sigBatch, sigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { - if htlcIsDust(false, false, feePerKw, htlc.Amount.ToSatoshis(), - dustLimit) { + if htlcIsDust( + chanType, false, false, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -2836,7 +2838,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // HTLC success transaction for them. The output of the timeout // transaction needs to account for fees, so we'll compute the // required fee and output now. - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee // With the proper output amount calculated, we can now @@ -3785,16 +3787,20 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // weight, needed to calculate the transaction fee. var totalHtlcWeight int64 for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(remoteChain, !remoteChain, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + lc.channelState.ChanType, remoteChain, !remoteChain, + feePerKw, htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } totalHtlcWeight += input.HTLCWeight } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(!remoteChain, !remoteChain, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + lc.channelState.ChanType, !remoteChain, !remoteChain, + feePerKw, htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -3857,7 +3863,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, Index: uint32(htlc.localOutputIndex), } - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee successTx, err := createHtlcSuccessTx( @@ -3911,7 +3917,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, Index: uint32(htlc.localOutputIndex), } - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee timeoutTx, err := createHtlcTimeoutTx( @@ -5383,7 +5389,7 @@ func newOutgoingHtlcResolution(signer input.Signer, // In order to properly reconstruct the HTLC transaction, we'll need to // re-calculate the fee required at this state, so we can add the // correct output value amount to the transaction. - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee // With the fee calculated, re-construct the second level timeout @@ -5513,7 +5519,7 @@ func newIncomingHtlcResolution(signer input.Signer, // First, we'll reconstruct the original HTLC success transaction, // taking into account the fee rate used. - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee successTx, err := createHtlcSuccessTx( chanType, op, secondLevelOutputAmt, csvDelay, @@ -5637,8 +5643,10 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, // We'll skip any HTLC's which were dust on the commitment // transaction, as these don't have a corresponding output // within the commitment transaction. - if htlcIsDust(htlc.Incoming, ourCommit, feePerKw, - htlc.Amt.ToSatoshis(), dustLimit) { + if htlcIsDust( + chanType, htlc.Incoming, ourCommit, feePerKw, + htlc.Amt.ToSatoshis(), dustLimit, + ) { continue } @@ -6141,7 +6149,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, // For an extra HTLC fee to be paid on our commitment, the HTLC must be // large enough to make a non-dust HTLC timeout transaction. htlcFee := lnwire.NewMSatFromSatoshis( - htlcTimeoutFee(feePerKw), + HtlcTimeoutFee(lc.channelState.ChanType, feePerKw), ) // If we are looking at the remote commitment, we must use the remote @@ -6151,7 +6159,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, lc.channelState.RemoteChanCfg.DustLimit, ) htlcFee = lnwire.NewMSatFromSatoshis( - htlcSuccessFee(feePerKw), + HtlcSuccessFee(lc.channelState.ChanType, feePerKw), ) } diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index c4c88670..511631bb 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -1011,7 +1011,8 @@ func TestHTLCDustLimit(t *testing.T) { // The amount of the HTLC should be above Alice's dust limit and below // Bob's dust limit. - htlcSat := (btcutil.Amount(500) + htlcTimeoutFee( + htlcSat := (btcutil.Amount(500) + HtlcTimeoutFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), @@ -1119,8 +1120,12 @@ func TestHTLCSigNumber(t *testing.T) { t.Fatalf("unable to get fee: %v", err) } - belowDust := btcutil.Amount(500) + htlcTimeoutFee(feePerKw) - aboveDust := btcutil.Amount(1400) + htlcSuccessFee(feePerKw) + belowDust := btcutil.Amount(500) + HtlcTimeoutFee( + channeldb.SingleFunderTweaklessBit, feePerKw, + ) + aboveDust := btcutil.Amount(1400) + HtlcSuccessFee( + channeldb.SingleFunderTweaklessBit, feePerKw, + ) // =================================================================== // Test that Bob will reject a commitment if Alice doesn't send enough @@ -1278,7 +1283,8 @@ func TestChannelBalanceDustLimit(t *testing.T) { defaultFee := calcStaticFee(1) aliceBalance := aliceChannel.channelState.LocalCommitment.LocalBalance.ToSatoshis() htlcSat := aliceBalance - defaultFee - htlcSat += htlcSuccessFee( + htlcSat += HtlcSuccessFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), @@ -4759,10 +4765,10 @@ func TestChanAvailableBalanceNearHtlcFee(t *testing.T) { aliceChannel.channelState.LocalCommitment.CommitFee, ) htlcTimeoutFee := lnwire.NewMSatFromSatoshis( - htlcTimeoutFee(feeRate), + HtlcTimeoutFee(aliceChannel.channelState.ChanType, feeRate), ) htlcSuccessFee := lnwire.NewMSatFromSatoshis( - htlcSuccessFee(feeRate), + HtlcSuccessFee(aliceChannel.channelState.ChanType, feeRate), ) // Helper method to check the current reported balance. @@ -6273,7 +6279,8 @@ func TestChanReserveLocalInitiatorDustHtlc(t *testing.T) { // limit (1300 sat). It is considered dust if the amount remaining // after paying the HTLC fee is below the dustlimit, so we choose a // size of 500+htlcFee. - htlcSat := btcutil.Amount(500) + htlcTimeoutFee( + htlcSat := btcutil.Amount(500) + HtlcTimeoutFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index abe9288e..077b6da9 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -255,6 +255,29 @@ func CommitWeight(chanType channeldb.ChannelType) int64 { return input.CommitWeight } +// HtlcTimeoutFee returns the fee in satoshis required for an HTLC timeout +// transaction based on the current fee rate. +func HtlcTimeoutFee(chanType channeldb.ChannelType, + feePerKw chainfee.SatPerKWeight) btcutil.Amount { + + if chanType.HasAnchors() { + return feePerKw.FeeForWeight(input.HtlcTimeoutWeightConfirmed) + } + + return feePerKw.FeeForWeight(input.HtlcTimeoutWeight) +} + +// HtlcSuccessFee returns the fee in satoshis required for an HTLC success +// transaction based on the current fee rate. +func HtlcSuccessFee(chanType channeldb.ChannelType, + feePerKw chainfee.SatPerKWeight) btcutil.Amount { + + if chanType.HasAnchors() { + return feePerKw.FeeForWeight(input.HtlcSuccessWeightConfirmed) + } + return feePerKw.FeeForWeight(input.HtlcSuccessWeight) +} + // CommitScriptAnchors return the scripts to use for the local and remote // anchor. func CommitScriptAnchors(localChanCfg, @@ -373,18 +396,20 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, numHTLCs := int64(0) for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(false, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { - + if htlcIsDust( + cb.chanState.ChanType, false, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } numHTLCs++ } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(true, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { - + if htlcIsDust( + cb.chanState.ChanType, true, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -460,8 +485,10 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // purposes of sorting. cltvs := make([]uint32, len(commitTx.TxOut)) for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(false, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + cb.chanState.ChanType, false, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -475,8 +502,10 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, cltvs = append(cltvs, htlc.Timeout) } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(true, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + cb.chanState.ChanType, true, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue }