diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index cb99a9b00..abfa892d9 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -3,7 +3,6 @@ package lnwallet import ( "bytes" "crypto/sha256" - "encoding/binary" "fmt" "math/big" @@ -22,28 +21,6 @@ var ( // SequenceLockTimeSeconds is the 22nd bit which indicates the lock // time is in seconds. SequenceLockTimeSeconds = uint32(1 << 22) - - // TimelockShift is used to make sure the commitment transaction is - // spendable by setting the locktime with it so that it is larger than - // 500,000,000, thus interpreting it as Unix epoch timestamp and not - // a block height. It is also smaller than the current timestamp which - // has bit (1 << 30) set, so there is no risk of having the commitment - // transaction be rejected. This way we can safely use the lower 24 bits - // of the locktime field for part of the obscured commitment transaction - // number. - TimelockShift = uint32(1 << 29) -) - -const ( - // StateHintSize is the total number of bytes used between the sequence - // number and locktime of the commitment transaction use to encode a hint - // to the state number of a particular commitment transaction. - StateHintSize = 6 - - // maxStateHint is the maximum state number we're able to encode using - // StateHintSize bytes amongst the sequence number and locktime fields - // of the commitment transaction. - maxStateHint uint64 = (1 << 48) - 1 ) // WitnessScriptHash generates a pay-to-witness-script-hash public key script @@ -622,108 +599,6 @@ func receiverHtlcSpendTimeout(signer Signer, signDesc *SignDescriptor, return witnessStack, nil } -// createHtlcTimeoutTx creates a transaction that spends the HTLC output on the -// commitment transaction of the peer that created an HTLC (the sender). This -// transaction essentially acts as an off-chain covenant as it spends a 2-of-2 -// multi-sig output. This output requires a signature from both the sender and -// receiver of the HTLC. By using a distinct transaction, we're able to -// uncouple the timeout and delay clauses of the HTLC contract. This -// transaction is locked with an absolute lock-time so the sender can only -// attempt to claim the output using it after the lock time has passed. -// -// In order to spend the HTLC output, the witness for the passed transaction -// should be: -// * <0> <0> -// -// 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, - cltvExpiry, csvDelay uint32, - revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { - - // Create a version two transaction (as the success version of this - // spends an output with a CSV timeout), and set the lock-time to the - // specified absolute lock-time in blocks. - timeoutTx := wire.NewMsgTx(2) - 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{ - PreviousOutPoint: htlcOutput, - }) - - // 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 - // HTLC outputs. - witnessScript, err := secondLevelHtlcScript(revocationKey, delayKey, - csvDelay) - if err != nil { - return nil, err - } - pkScript, err := WitnessScriptHash(witnessScript) - if err != nil { - return nil, err - } - - // Finally, the output is simply the amount of the HTLC (minus the - // required fees), paying to the regular second level HTLC script. - timeoutTx.AddTxOut(&wire.TxOut{ - Value: int64(htlcAmt), - PkScript: pkScript, - }) - - return timeoutTx, nil -} - -// createHtlcSuccessTx creates a transaction that spends the output on the -// commitment transaction of the peer that receives an HTLC. This transaction -// essentially acts as an off-chain covenant as it's only permitted to spend -// the designated HTLC output, and also that spend can _only_ be used as a -// state transition to create another output which actually allows redemption -// or revocation of an HTLC. -// -// 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, - revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { - - // Create a version two transaction (as the success version of this - // spends an output with a CSV timeout). - 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{ - PreviousOutPoint: htlcOutput, - }) - - // 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 - // HTLC outputs. - witnessScript, err := secondLevelHtlcScript(revocationKey, delayKey, - csvDelay) - if err != nil { - return nil, err - } - pkScript, err := WitnessScriptHash(witnessScript) - if err != nil { - return nil, err - } - - // Finally, the output is simply the amount of the HTLC (minus the - // required fees), paying to the timeout script. - successTx.AddTxOut(&wire.TxOut{ - Value: int64(htlcAmt), - PkScript: pkScript, - }) - - return successTx, nil -} - // secondLevelHtlcScript is the uniform script that's used as the output for // the second-level HTLC transactions. The second level transaction act as a // sort of covenant, ensuring that a 2-of-2 multi-sig output can only be @@ -1235,71 +1110,6 @@ func DeriveRevocationPrivKey(revokeBasePriv *btcec.PrivateKey, return priv } -// SetStateNumHint encodes the current state number within the passed -// commitment transaction by re-purposing the locktime and sequence fields in -// the commitment transaction to encode the obfuscated state number. The state -// number is encoded using 48 bits. The lower 24 bits of the lock time are the -// lower 24 bits of the obfuscated state number and the lower 24 bits of the -// sequence field are the higher 24 bits. Finally before encoding, the -// obfuscator is XOR'd against the state number in order to hide the exact -// state number from the PoV of outside parties. -func SetStateNumHint(commitTx *wire.MsgTx, stateNum uint64, - obfuscator [StateHintSize]byte) error { - - // With the current schema we are only able to encode state num - // hints up to 2^48. Therefore if the passed height is greater than our - // state hint ceiling, then exit early. - if stateNum > maxStateHint { - return fmt.Errorf("unable to encode state, %v is greater "+ - "state num that max of %v", stateNum, maxStateHint) - } - - if len(commitTx.TxIn) != 1 { - return fmt.Errorf("commitment tx must have exactly 1 input, "+ - "instead has %v", len(commitTx.TxIn)) - } - - // Convert the obfuscator into a uint64, then XOR that against the - // targeted height in order to obfuscate the state number of the - // commitment transaction in the case that either commitment - // transaction is broadcast directly on chain. - var obfs [8]byte - copy(obfs[2:], obfuscator[:]) - xorInt := binary.BigEndian.Uint64(obfs[:]) - - stateNum = stateNum ^ xorInt - - // Set the height bit of the sequence number in order to disable any - // sequence locks semantics. - commitTx.TxIn[0].Sequence = uint32(stateNum>>24) | wire.SequenceLockTimeDisabled - commitTx.LockTime = uint32(stateNum&0xFFFFFF) | TimelockShift - - return nil -} - -// GetStateNumHint recovers the current state number given a commitment -// transaction which has previously had the state number encoded within it via -// setStateNumHint and a shared obfuscator. -// -// See setStateNumHint for further details w.r.t exactly how the state-hints -// are encoded. -func GetStateNumHint(commitTx *wire.MsgTx, obfuscator [StateHintSize]byte) uint64 { - // Convert the obfuscator into a uint64, this will be used to - // de-obfuscate the final recovered state number. - var obfs [8]byte - copy(obfs[2:], obfuscator[:]) - xorInt := binary.BigEndian.Uint64(obfs[:]) - - // Retrieve the state hint from the sequence number and locktime - // of the transaction. - stateNumXor := uint64(commitTx.TxIn[0].Sequence&0xFFFFFF) << 24 - stateNumXor |= uint64(commitTx.LockTime & 0xFFFFFF) - - // Finally, to obtain the final state number, we XOR by the obfuscator - // value to de-obfuscate the state number. - return stateNumXor ^ xorInt -} - // ComputeCommitmentPoint generates a commitment point given a commitment // secret. The commitment point for each state is used to randomize each key in // the key-ring and also to used as a tweak to derive new public+private keys diff --git a/lnwallet/script_utils_test.go b/lnwallet/script_utils_test.go index 7adacba47..355254852 100644 --- a/lnwallet/script_utils_test.go +++ b/lnwallet/script_utils_test.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "testing" - "time" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -16,211 +15,6 @@ import ( "github.com/lightningnetwork/lnd/keychain" ) -// TestCommitmentSpendValidation test the spendability of both outputs within -// the commitment transaction. -// -// The following spending cases are covered by this test: -// * Alice's spend from the delayed output on her commitment transaction. -// * Bob's spend from Alice's delayed output when she broadcasts a revoked -// commitment transaction. -// * Bob's spend from his unencumbered output within Alice's commitment -// transaction. -func TestCommitmentSpendValidation(t *testing.T) { - t.Parallel() - - // We generate a fake output, and the corresponding txin. This output - // doesn't need to exist, as we'll only be validating spending from the - // transaction that references this. - txid, err := chainhash.NewHash(testHdSeed.CloneBytes()) - if err != nil { - t.Fatalf("unable to create txid: %v", err) - } - fundingOut := &wire.OutPoint{ - Hash: *txid, - Index: 50, - } - fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil) - - const channelBalance = btcutil.Amount(1 * 10e8) - const csvTimeout = uint32(5) - - // We also set up set some resources for the commitment transaction. - // Each side currently has 1 BTC within the channel, with a total - // channel capacity of 2BTC. - aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), - testWalletPrivKey) - bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), - bobsPrivKey) - - revocationPreimage := testHdSeed.CloneBytes() - commitSecret, commitPoint := btcec.PrivKeyFromBytes(btcec.S256(), - revocationPreimage) - revokePubKey := DeriveRevocationPubkey(bobKeyPub, commitPoint) - - aliceDelayKey := TweakPubKey(aliceKeyPub, commitPoint) - bobPayKey := TweakPubKey(bobKeyPub, commitPoint) - - aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub) - bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub) - - aliceSelfOutputSigner := &mockSigner{ - privkeys: []*btcec.PrivateKey{aliceKeyPriv}, - } - - // With all the test data set up, we create the commitment transaction. - // We only focus on a single party's transactions, as the scripts are - // identical with the roles reversed. - // - // This is Alice's commitment transaction, so she must wait a CSV delay - // of 5 blocks before sweeping the output, while bob can spend - // immediately with either the revocation key, or his regular key. - keyRing := &CommitmentKeyRing{ - DelayKey: aliceDelayKey, - RevocationKey: revokePubKey, - NoDelayKey: bobPayKey, - } - commitmentTx, err := CreateCommitTx(*fakeFundingTxIn, keyRing, csvTimeout, - channelBalance, channelBalance, DefaultDustLimit()) - if err != nil { - t.Fatalf("unable to create commitment transaction: %v", nil) - } - - delayOutput := commitmentTx.TxOut[0] - regularOutput := commitmentTx.TxOut[1] - - // We're testing an uncooperative close, output sweep, so construct a - // transaction which sweeps the funds to a random address. - targetOutput, err := CommitScriptUnencumbered(aliceKeyPub) - if err != nil { - t.Fatalf("unable to create target output: %v", err) - } - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{ - Hash: commitmentTx.TxHash(), - Index: 0, - }, nil, nil)) - sweepTx.AddTxOut(&wire.TxOut{ - PkScript: targetOutput, - Value: 0.5 * 10e8, - }) - - // First, we'll test spending with Alice's key after the timeout. - delayScript, err := CommitScriptToSelf(csvTimeout, aliceDelayKey, - revokePubKey) - if err != nil { - t.Fatalf("unable to generate alice delay script: %v", err) - } - sweepTx.TxIn[0].Sequence = lockTimeToSequence(false, csvTimeout) - signDesc := &SignDescriptor{ - WitnessScript: delayScript, - KeyDesc: keychain.KeyDescriptor{ - PubKey: aliceKeyPub, - }, - SingleTweak: aliceCommitTweak, - SigHashes: txscript.NewTxSigHashes(sweepTx), - Output: &wire.TxOut{ - Value: int64(channelBalance), - }, - HashType: txscript.SigHashAll, - InputIndex: 0, - } - aliceWitnessSpend, err := CommitSpendTimeout(aliceSelfOutputSigner, - signDesc, sweepTx) - if err != nil { - t.Fatalf("unable to generate delay commit spend witness: %v", err) - } - sweepTx.TxIn[0].Witness = aliceWitnessSpend - vm, err := txscript.NewEngine(delayOutput.PkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(channelBalance)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) - } - if err := vm.Execute(); err != nil { - t.Fatalf("spend from delay output is invalid: %v", err) - } - - bobSigner := &mockSigner{privkeys: []*btcec.PrivateKey{bobKeyPriv}} - - // Next, we'll test bob spending with the derived revocation key to - // simulate the scenario when Alice broadcasts this commitment - // transaction after it's been revoked. - signDesc = &SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - PubKey: bobKeyPub, - }, - DoubleTweak: commitSecret, - WitnessScript: delayScript, - SigHashes: txscript.NewTxSigHashes(sweepTx), - Output: &wire.TxOut{ - Value: int64(channelBalance), - }, - HashType: txscript.SigHashAll, - InputIndex: 0, - } - bobWitnessSpend, err := CommitSpendRevoke(bobSigner, signDesc, - sweepTx) - if err != nil { - t.Fatalf("unable to generate revocation witness: %v", err) - } - sweepTx.TxIn[0].Witness = bobWitnessSpend - vm, err = txscript.NewEngine(delayOutput.PkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(channelBalance)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) - } - if err := vm.Execute(); err != nil { - t.Fatalf("revocation spend is invalid: %v", err) - } - - // In order to test the final scenario, we modify the TxIn of the sweep - // transaction to instead point to the regular output (non delay) - // within the commitment transaction. - sweepTx.TxIn[0] = &wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: commitmentTx.TxHash(), - Index: 1, - }, - } - - // Finally, we test bob sweeping his output as normal in the case that - // Alice broadcasts this commitment transaction. - bobScriptP2WKH, err := CommitScriptUnencumbered(bobPayKey) - if err != nil { - t.Fatalf("unable to create bob p2wkh script: %v", err) - } - signDesc = &SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - PubKey: bobKeyPub, - }, - SingleTweak: bobCommitTweak, - WitnessScript: bobScriptP2WKH, - SigHashes: txscript.NewTxSigHashes(sweepTx), - Output: &wire.TxOut{ - Value: int64(channelBalance), - PkScript: bobScriptP2WKH, - }, - HashType: txscript.SigHashAll, - InputIndex: 0, - } - bobRegularSpend, err := CommitSpendNoDelay(bobSigner, signDesc, - sweepTx) - if err != nil { - t.Fatalf("unable to create bob regular spend: %v", err) - } - sweepTx.TxIn[0].Witness = bobRegularSpend - vm, err = txscript.NewEngine(regularOutput.PkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(channelBalance)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) - } - if err := vm.Execute(); err != nil { - t.Fatalf("bob p2wkh spend is invalid: %v", err) - } -} - // TestRevocationKeyDerivation tests that given a public key, and a revocation // hash, the homomorphic revocation public and private key derivation work // properly. @@ -1053,97 +847,6 @@ func TestSecondLevelHtlcSpends(t *testing.T) { } } -func TestCommitTxStateHint(t *testing.T) { - t.Parallel() - - stateHintTests := []struct { - name string - from uint64 - to uint64 - inputs int - shouldFail bool - }{ - { - name: "states 0 to 1000", - from: 0, - to: 1000, - inputs: 1, - shouldFail: false, - }, - { - name: "states 'maxStateHint-1000' to 'maxStateHint'", - from: maxStateHint - 1000, - to: maxStateHint, - inputs: 1, - shouldFail: false, - }, - { - name: "state 'maxStateHint+1'", - from: maxStateHint + 1, - to: maxStateHint + 10, - inputs: 1, - shouldFail: true, - }, - { - name: "commit transaction with two inputs", - inputs: 2, - shouldFail: true, - }, - } - - var obfuscator [StateHintSize]byte - copy(obfuscator[:], testHdSeed[:StateHintSize]) - timeYesterday := uint32(time.Now().Unix() - 24*60*60) - - for _, test := range stateHintTests { - commitTx := wire.NewMsgTx(2) - - // Add supplied number of inputs to the commitment transaction. - for i := 0; i < test.inputs; i++ { - commitTx.AddTxIn(&wire.TxIn{}) - } - - for i := test.from; i <= test.to; i++ { - stateNum := uint64(i) - - err := SetStateNumHint(commitTx, stateNum, obfuscator) - if err != nil && !test.shouldFail { - t.Fatalf("unable to set state num %v: %v", i, err) - } else if err == nil && test.shouldFail { - t.Fatalf("Failed(%v): test should fail but did not", test.name) - } - - locktime := commitTx.LockTime - sequence := commitTx.TxIn[0].Sequence - - // Locktime should not be less than 500,000,000 and not larger - // than the time 24 hours ago. One day should provide a good - // enough buffer for the tests. - if locktime < 5e8 || locktime > timeYesterday { - if !test.shouldFail { - t.Fatalf("The value of locktime (%v) may cause the commitment "+ - "transaction to be unspendable", locktime) - } - } - - if sequence&wire.SequenceLockTimeDisabled == 0 { - if !test.shouldFail { - t.Fatalf("Sequence locktime is NOT disabled when it should be") - } - } - - extractedStateNum := GetStateNumHint(commitTx, obfuscator) - if extractedStateNum != stateNum && !test.shouldFail { - t.Fatalf("state number mismatched, expected %v, got %v", - stateNum, extractedStateNum) - } else if extractedStateNum == stateNum && test.shouldFail { - t.Fatalf("Failed(%v): test should fail but did not", test.name) - } - } - t.Logf("Passed: %v", test.name) - } -} - // TestSpecificationKeyDerivation implements the test vectors provided in // BOLT-03, Appendix E. func TestSpecificationKeyDerivation(t *testing.T) { diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go new file mode 100644 index 000000000..bbbe2128a --- /dev/null +++ b/lnwallet/transactions.go @@ -0,0 +1,201 @@ +package lnwallet + +import ( + "encoding/binary" + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +const ( + // StateHintSize is the total number of bytes used between the sequence + // number and locktime of the commitment transaction use to encode a hint + // to the state number of a particular commitment transaction. + StateHintSize = 6 + + // MaxStateHint is the maximum state number we're able to encode using + // StateHintSize bytes amongst the sequence number and locktime fields + // of the commitment transaction. + maxStateHint uint64 = (1 << 48) - 1 +) + +var ( + // TimelockShift is used to make sure the commitment transaction is + // spendable by setting the locktime with it so that it is larger than + // 500,000,000, thus interpreting it as Unix epoch timestamp and not + // a block height. It is also smaller than the current timestamp which + // has bit (1 << 30) set, so there is no risk of having the commitment + // transaction be rejected. This way we can safely use the lower 24 bits + // of the locktime field for part of the obscured commitment transaction + // number. + TimelockShift = uint32(1 << 29) +) + +// createHtlcSuccessTx creates a transaction that spends the output on the +// commitment transaction of the peer that receives an HTLC. This transaction +// essentially acts as an off-chain covenant as it's only permitted to spend +// the designated HTLC output, and also that spend can _only_ be used as a +// state transition to create another output which actually allows redemption +// or revocation of an HTLC. +// +// 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, + revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { + + // Create a version two transaction (as the success version of this + // spends an output with a CSV timeout). + 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{ + PreviousOutPoint: htlcOutput, + }) + + // 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 + // HTLC outputs. + witnessScript, err := secondLevelHtlcScript(revocationKey, delayKey, + csvDelay) + if err != nil { + return nil, err + } + pkScript, err := WitnessScriptHash(witnessScript) + if err != nil { + return nil, err + } + + // Finally, the output is simply the amount of the HTLC (minus the + // required fees), paying to the timeout script. + successTx.AddTxOut(&wire.TxOut{ + Value: int64(htlcAmt), + PkScript: pkScript, + }) + + return successTx, nil +} + +// createHtlcTimeoutTx creates a transaction that spends the HTLC output on the +// commitment transaction of the peer that created an HTLC (the sender). This +// transaction essentially acts as an off-chain covenant as it spends a 2-of-2 +// multi-sig output. This output requires a signature from both the sender and +// receiver of the HTLC. By using a distinct transaction, we're able to +// uncouple the timeout and delay clauses of the HTLC contract. This +// transaction is locked with an absolute lock-time so the sender can only +// attempt to claim the output using it after the lock time has passed. +// +// In order to spend the HTLC output, the witness for the passed transaction +// should be: +// * <0> <0> +// +// 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, + cltvExpiry, csvDelay uint32, + revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { + + // Create a version two transaction (as the success version of this + // spends an output with a CSV timeout), and set the lock-time to the + // specified absolute lock-time in blocks. + timeoutTx := wire.NewMsgTx(2) + 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{ + PreviousOutPoint: htlcOutput, + }) + + // 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 + // HTLC outputs. + witnessScript, err := secondLevelHtlcScript(revocationKey, delayKey, + csvDelay) + if err != nil { + return nil, err + } + pkScript, err := WitnessScriptHash(witnessScript) + if err != nil { + return nil, err + } + + // Finally, the output is simply the amount of the HTLC (minus the + // required fees), paying to the regular second level HTLC script. + timeoutTx.AddTxOut(&wire.TxOut{ + Value: int64(htlcAmt), + PkScript: pkScript, + }) + + return timeoutTx, nil +} + +// SetStateNumHint encodes the current state number within the passed +// commitment transaction by re-purposing the locktime and sequence fields in +// the commitment transaction to encode the obfuscated state number. The state +// number is encoded using 48 bits. The lower 24 bits of the lock time are the +// lower 24 bits of the obfuscated state number and the lower 24 bits of the +// sequence field are the higher 24 bits. Finally before encoding, the +// obfuscator is XOR'd against the state number in order to hide the exact +// state number from the PoV of outside parties. +func SetStateNumHint(commitTx *wire.MsgTx, stateNum uint64, + obfuscator [StateHintSize]byte) error { + + // With the current schema we are only able to encode state num + // hints up to 2^48. Therefore if the passed height is greater than our + // state hint ceiling, then exit early. + if stateNum > maxStateHint { + return fmt.Errorf("unable to encode state, %v is greater "+ + "state num that max of %v", stateNum, maxStateHint) + } + + if len(commitTx.TxIn) != 1 { + return fmt.Errorf("commitment tx must have exactly 1 input, "+ + "instead has %v", len(commitTx.TxIn)) + } + + // Convert the obfuscator into a uint64, then XOR that against the + // targeted height in order to obfuscate the state number of the + // commitment transaction in the case that either commitment + // transaction is broadcast directly on chain. + var obfs [8]byte + copy(obfs[2:], obfuscator[:]) + xorInt := binary.BigEndian.Uint64(obfs[:]) + + stateNum = stateNum ^ xorInt + + // Set the height bit of the sequence number in order to disable any + // sequence locks semantics. + commitTx.TxIn[0].Sequence = uint32(stateNum>>24) | wire.SequenceLockTimeDisabled + commitTx.LockTime = uint32(stateNum&0xFFFFFF) | TimelockShift + + return nil +} + +// GetStateNumHint recovers the current state number given a commitment +// transaction which has previously had the state number encoded within it via +// setStateNumHint and a shared obfuscator. +// +// See setStateNumHint for further details w.r.t exactly how the state-hints +// are encoded. +func GetStateNumHint(commitTx *wire.MsgTx, obfuscator [StateHintSize]byte) uint64 { + // Convert the obfuscator into a uint64, this will be used to + // de-obfuscate the final recovered state number. + var obfs [8]byte + copy(obfs[2:], obfuscator[:]) + xorInt := binary.BigEndian.Uint64(obfs[:]) + + // Retrieve the state hint from the sequence number and locktime + // of the transaction. + stateNumXor := uint64(commitTx.TxIn[0].Sequence&0xFFFFFF) << 24 + stateNumXor |= uint64(commitTx.LockTime & 0xFFFFFF) + + // Finally, to obtain the final state number, we XOR by the obfuscator + // value to de-obfuscate the state number. + return stateNumXor ^ xorInt +} diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 2960b4d18..f592ca8e1 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -5,10 +5,12 @@ import ( "encoding/hex" "fmt" "testing" + "time" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" @@ -910,3 +912,299 @@ func htlcViewFromHTLCs(htlcs []channeldb.HTLC) *htlcView { } return &theHTLCView } + +func TestCommitTxStateHint(t *testing.T) { + t.Parallel() + + stateHintTests := []struct { + name string + from uint64 + to uint64 + inputs int + shouldFail bool + }{ + { + name: "states 0 to 1000", + from: 0, + to: 1000, + inputs: 1, + shouldFail: false, + }, + { + name: "states 'maxStateHint-1000' to 'maxStateHint'", + from: maxStateHint - 1000, + to: maxStateHint, + inputs: 1, + shouldFail: false, + }, + { + name: "state 'maxStateHint+1'", + from: maxStateHint + 1, + to: maxStateHint + 10, + inputs: 1, + shouldFail: true, + }, + { + name: "commit transaction with two inputs", + inputs: 2, + shouldFail: true, + }, + } + + var obfuscator [StateHintSize]byte + copy(obfuscator[:], testHdSeed[:StateHintSize]) + timeYesterday := uint32(time.Now().Unix() - 24*60*60) + + for _, test := range stateHintTests { + commitTx := wire.NewMsgTx(2) + + // Add supplied number of inputs to the commitment transaction. + for i := 0; i < test.inputs; i++ { + commitTx.AddTxIn(&wire.TxIn{}) + } + + for i := test.from; i <= test.to; i++ { + stateNum := uint64(i) + + err := SetStateNumHint(commitTx, stateNum, obfuscator) + if err != nil && !test.shouldFail { + t.Fatalf("unable to set state num %v: %v", i, err) + } else if err == nil && test.shouldFail { + t.Fatalf("Failed(%v): test should fail but did not", test.name) + } + + locktime := commitTx.LockTime + sequence := commitTx.TxIn[0].Sequence + + // Locktime should not be less than 500,000,000 and not larger + // than the time 24 hours ago. One day should provide a good + // enough buffer for the tests. + if locktime < 5e8 || locktime > timeYesterday { + if !test.shouldFail { + t.Fatalf("The value of locktime (%v) may cause the commitment "+ + "transaction to be unspendable", locktime) + } + } + + if sequence&wire.SequenceLockTimeDisabled == 0 { + if !test.shouldFail { + t.Fatalf("Sequence locktime is NOT disabled when it should be") + } + } + + extractedStateNum := GetStateNumHint(commitTx, obfuscator) + if extractedStateNum != stateNum && !test.shouldFail { + t.Fatalf("state number mismatched, expected %v, got %v", + stateNum, extractedStateNum) + } else if extractedStateNum == stateNum && test.shouldFail { + t.Fatalf("Failed(%v): test should fail but did not", test.name) + } + } + t.Logf("Passed: %v", test.name) + } +} + +// TestCommitmentSpendValidation test the spendability of both outputs within +// the commitment transaction. +// +// The following spending cases are covered by this test: +// * Alice's spend from the delayed output on her commitment transaction. +// * Bob's spend from Alice's delayed output when she broadcasts a revoked +// commitment transaction. +// * Bob's spend from his unencumbered output within Alice's commitment +// transaction. +func TestCommitmentSpendValidation(t *testing.T) { + t.Parallel() + + // We generate a fake output, and the corresponding txin. This output + // doesn't need to exist, as we'll only be validating spending from the + // transaction that references this. + txid, err := chainhash.NewHash(testHdSeed.CloneBytes()) + if err != nil { + t.Fatalf("unable to create txid: %v", err) + } + fundingOut := &wire.OutPoint{ + Hash: *txid, + Index: 50, + } + fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil) + + const channelBalance = btcutil.Amount(1 * 10e8) + const csvTimeout = uint32(5) + + // We also set up set some resources for the commitment transaction. + // Each side currently has 1 BTC within the channel, with a total + // channel capacity of 2BTC. + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + testWalletPrivKey) + bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + bobsPrivKey) + + revocationPreimage := testHdSeed.CloneBytes() + commitSecret, commitPoint := btcec.PrivKeyFromBytes(btcec.S256(), + revocationPreimage) + revokePubKey := DeriveRevocationPubkey(bobKeyPub, commitPoint) + + aliceDelayKey := TweakPubKey(aliceKeyPub, commitPoint) + bobPayKey := TweakPubKey(bobKeyPub, commitPoint) + + aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub) + bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub) + + aliceSelfOutputSigner := &mockSigner{ + privkeys: []*btcec.PrivateKey{aliceKeyPriv}, + } + + // With all the test data set up, we create the commitment transaction. + // We only focus on a single party's transactions, as the scripts are + // identical with the roles reversed. + // + // This is Alice's commitment transaction, so she must wait a CSV delay + // of 5 blocks before sweeping the output, while bob can spend + // immediately with either the revocation key, or his regular key. + keyRing := &CommitmentKeyRing{ + DelayKey: aliceDelayKey, + RevocationKey: revokePubKey, + NoDelayKey: bobPayKey, + } + commitmentTx, err := CreateCommitTx(*fakeFundingTxIn, keyRing, csvTimeout, + channelBalance, channelBalance, DefaultDustLimit()) + if err != nil { + t.Fatalf("unable to create commitment transaction: %v", nil) + } + + delayOutput := commitmentTx.TxOut[0] + regularOutput := commitmentTx.TxOut[1] + + // We're testing an uncooperative close, output sweep, so construct a + // transaction which sweeps the funds to a random address. + targetOutput, err := CommitScriptUnencumbered(aliceKeyPub) + if err != nil { + t.Fatalf("unable to create target output: %v", err) + } + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{ + Hash: commitmentTx.TxHash(), + Index: 0, + }, nil, nil)) + sweepTx.AddTxOut(&wire.TxOut{ + PkScript: targetOutput, + Value: 0.5 * 10e8, + }) + + // First, we'll test spending with Alice's key after the timeout. + delayScript, err := CommitScriptToSelf(csvTimeout, aliceDelayKey, + revokePubKey) + if err != nil { + t.Fatalf("unable to generate alice delay script: %v", err) + } + sweepTx.TxIn[0].Sequence = lockTimeToSequence(false, csvTimeout) + signDesc := &SignDescriptor{ + WitnessScript: delayScript, + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + SigHashes: txscript.NewTxSigHashes(sweepTx), + Output: &wire.TxOut{ + Value: int64(channelBalance), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + } + aliceWitnessSpend, err := CommitSpendTimeout(aliceSelfOutputSigner, + signDesc, sweepTx) + if err != nil { + t.Fatalf("unable to generate delay commit spend witness: %v", err) + } + sweepTx.TxIn[0].Witness = aliceWitnessSpend + vm, err := txscript.NewEngine(delayOutput.PkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(channelBalance)) + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + if err := vm.Execute(); err != nil { + t.Fatalf("spend from delay output is invalid: %v", err) + } + + bobSigner := &mockSigner{privkeys: []*btcec.PrivateKey{bobKeyPriv}} + + // Next, we'll test bob spending with the derived revocation key to + // simulate the scenario when Alice broadcasts this commitment + // transaction after it's been revoked. + signDesc = &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + DoubleTweak: commitSecret, + WitnessScript: delayScript, + SigHashes: txscript.NewTxSigHashes(sweepTx), + Output: &wire.TxOut{ + Value: int64(channelBalance), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + } + bobWitnessSpend, err := CommitSpendRevoke(bobSigner, signDesc, + sweepTx) + if err != nil { + t.Fatalf("unable to generate revocation witness: %v", err) + } + sweepTx.TxIn[0].Witness = bobWitnessSpend + vm, err = txscript.NewEngine(delayOutput.PkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(channelBalance)) + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + if err := vm.Execute(); err != nil { + t.Fatalf("revocation spend is invalid: %v", err) + } + + // In order to test the final scenario, we modify the TxIn of the sweep + // transaction to instead point to the regular output (non delay) + // within the commitment transaction. + sweepTx.TxIn[0] = &wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: commitmentTx.TxHash(), + Index: 1, + }, + } + + // Finally, we test bob sweeping his output as normal in the case that + // Alice broadcasts this commitment transaction. + bobScriptP2WKH, err := CommitScriptUnencumbered(bobPayKey) + if err != nil { + t.Fatalf("unable to create bob p2wkh script: %v", err) + } + signDesc = &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: bobScriptP2WKH, + SigHashes: txscript.NewTxSigHashes(sweepTx), + Output: &wire.TxOut{ + Value: int64(channelBalance), + PkScript: bobScriptP2WKH, + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + } + bobRegularSpend, err := CommitSpendNoDelay(bobSigner, signDesc, + sweepTx) + if err != nil { + t.Fatalf("unable to create bob regular spend: %v", err) + } + sweepTx.TxIn[0].Witness = bobRegularSpend + vm, err = txscript.NewEngine(regularOutput.PkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(channelBalance)) + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + if err := vm.Execute(); err != nil { + t.Fatalf("bob p2wkh spend is invalid: %v", err) + } +}