From 405a435a84dcdc24dff6e077e8bdf6748ac48426 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 1 Mar 2023 21:48:28 -0800 Subject: [PATCH] input: add weight estimation + tests for all taproot witness gen types --- input/size.go | 268 ++++++++++++++++ input/size_test.go | 690 ++++++++++++++++++++++++++++++++++++---- input/taproot_test.go | 10 +- input/witnessgen.go | 48 ++- lnwallet/reservation.go | 4 +- 5 files changed, 956 insertions(+), 64 deletions(-) diff --git a/input/size.go b/input/size.go index 9796314cb..4b033002b 100644 --- a/input/size.go +++ b/input/size.go @@ -153,6 +153,12 @@ const ( // - PkScript (P2WSH) CommitmentDelayOutput = 8 + 1 + P2WSHSize + // TaprootCommitmentOutput 43 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (P2TR) + TaprootCommitmentOutput = 8 + 1 + P2TRSize + // CommitmentKeyHashOutput 31 bytes // - Value: 8 bytes // - VarInt: 1 byte (PkScript length) @@ -165,6 +171,12 @@ const ( // - PkScript (P2WSH) CommitmentAnchorOutput = 8 + 1 + P2WSHSize + // TaprootCommitmentAnchorOutput 43 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (P2TR) + TaprootCommitmentAnchorOutput = 8 + 1 + P2TRSize + // HTLCSize 43 bytes // - Value: 8 bytes // - VarInt: 1 byte (PkScript length) @@ -222,12 +234,32 @@ const ( // BaseAnchorCommitmentTxWeight 900 weight. BaseAnchorCommitmentTxWeight = witnessScaleFactor * BaseAnchorCommitmentTxSize + // BaseTaprootCommitmentTxWeight 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: 172 + 43 * num-htlc-outputs bytes + // OutputPayingToThem, + // OutputPayingToUs, + // ....HTLCOutputs... + // - LockTime: 4 bytes + BaseTaprootCommitmentTxWeight = (4 + 1 + FundingInputSize + 3 + + 2*TaprootCommitmentOutput + 2*TaprootCommitmentAnchorOutput + 4) * + witnessScaleFactor + // CommitWeight 724 weight. CommitWeight = BaseCommitmentTxWeight + WitnessCommitmentTxWeight // AnchorCommitWeight 1124 weight. AnchorCommitWeight = BaseAnchorCommitmentTxWeight + WitnessCommitmentTxWeight + // TaprootCommitWeight 968 weight. + TaprootCommitWeight = (BaseTaprootCommitmentTxWeight + + WitnessHeaderSize + TaprootKeyPathWitnessSize) + // HTLCWeight 172 weight. HTLCWeight = witnessScaleFactor * HTLCSize @@ -236,11 +268,19 @@ const ( // which will transition an outgoing HTLC to the delay-and-claim state. HtlcTimeoutWeight = 663 + // TaprootHtlcTimeoutWeight is the total weight of the taproot HTLC + // timeout transaction. + TaprootHtlcTimeoutWeight = 645 + // HtlcSuccessWeight 703 weight // HtlcSuccessWeight is the weight of the HTLC success transaction // which will transition an incoming HTLC to the delay-and-claim state. HtlcSuccessWeight = 703 + // TaprootHtlcSuccessWeight is the total weight of the taproot HTLC + // success transaction. + TaprootHtlcSuccessWeight = 705 + // HtlcConfirmedScriptOverhead 3 bytes // HtlcConfirmedScriptOverhead is the extra length of an HTLC script // that requires confirmation before it can be spent. These extra bytes @@ -556,6 +596,232 @@ const ( // - leafVersionAndParity: 1 byte // - schnorrPubKey: 32 byte TaprootBaseControlBlockWitnessSize = 33 + + // TaprootToLocalScriptSize + // - OP_DATA: 1 byte (pub key len) + // - local_key: 32 bytes + // - OP_CHECKSIG: 1 byte + // - OP_DATA: 1 byte (csv delay) + // - csv_delay: 4 bytes (worst case estimate) + // - OP_CSV: 1 byte + // - OP_DROP: 1 byte + TaprootToLocalScriptSize = 41 + + // TaprootToLocalWitnessSize: 175 bytes + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - script_len: 1 byte + // - taproot_to_local_script_size: 41 bytes + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + // - sibling_merkle_hash: 32 bytes + TaprootToLocalWitnessSize = 1 + 1 + 65 + 1 + TaprootToLocalScriptSize + + 1 + TaprootBaseControlBlockWitnessSize + 32 + + // TaprootToLocalRevokeScriptSize: 68 bytes + // - OP_DATA: 1 byte + // - local key: 32 bytes + // - OP_DROP: 1 byte + // - OP_DATA: 1 byte + // - revocation key: 32 bytes + // - OP_CHECKSIG: 1 byte + TaprootToLocalRevokeScriptSize = 1 + 32 + 1 + 1 + 32 + 1 + + // TaprootToLocalRevokeWitnessSize: 202 bytes + // - NumberOfWitnessElements: 1 byte + // - sigLength: 1 byte + // - sweep sig: 65 bytes + // - script len: 1 byte + // - revocation script size: 68 bytes + // - ctrl block size: 1 byte + // - base control block: 33 bytes + // - merkle proof: 32 + TaprootToLocalRevokeWitnessSize = 1 + 1 + 65 + 1 + TaprootToLocalRevokeScriptSize + 1 + 33 + 32 + + // TaprootToRemoteScriptSize + // - OP_DATA: 1 byte + // - remote key: 32 bytes + // - OP_CHECKSIG: 1 byte + // - OP_1: 1 byte + // - OP_CHECKSEQUENCEVERIFY: 1 byte + // - OP_DROP: 1 byte + TaprootToRemoteScriptSize = 1 + 32 + 1 + 1 + 1 + 1 + + // TaprootToRemoteWitnessSize: + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - script_len: 1 byte + // - taproot_to_local_script_size: 36 bytes + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + TaprootToRemoteWitnessSize = 1 + 1 + 65 + 1 + TaprootToRemoteScriptSize + + 1 + TaprootBaseControlBlockWitnessSize + + // TaprootAnchorWitnessSize: 67 bytes + // + // In this case, we use the custom sighash size to give the most + // pessemistic estimate. + TaprootAnchorWitnessSize = TaprootKeyPathCustomSighashWitnessSize + + // TaprootSecondLevelHtlcScriptSize: 41 bytes + // - OP_DATA: 1 byte (pub key len) + // - local_key: 32 bytes + // - OP_CHECKSIG: 1 byte + // - OP_DATA: 1 byte (csv delay) + // - csv_delay: 4 bytes (worst case) + // - OP_CSV: 1 byte + // - OP_DROP: 1 byte + TaprootSecondLevelHtlcScriptSize = 1 + 32 + 1 + 1 + 4 + 1 + 1 + + // TaprootSecondLevelHtlcWitnessSize: + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - script_len: 1 byte + // - taproot_second_level_htlc_script_size: 40 bytes + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + TaprootSecondLevelHtlcWitnessSize = 1 + 1 + 65 + 1 + + TaprootSecondLevelHtlcScriptSize + 1 + + TaprootBaseControlBlockWitnessSize + + // TaprootSecondLevelRevokeWitnessSize + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + TaprootSecondLevelRevokeWitnessSize = TaprootKeyPathCustomSighashWitnessSize + + // TaprootAcceptedRevokeWitnessSize: + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + TaprootAcceptedRevokeWitnessSize = TaprootKeyPathCustomSighashWitnessSize + + // TaprootOfferedRevokeWitnessSize: + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + TaprootOfferedRevokeWitnessSize = TaprootKeyPathCustomSighashWitnessSize + + // TaprootHtlcOfferedRemoteTimeoutScriptSize: 42 bytes + // - OP_DATA: 1 byte (pub key len) + // - local_key: 32 bytes + // - OP_CHECKSIG: 1 byte + // - OP_1: 1 byte + // - OP_DROP: 1 byte + // - OP_CHECKSEQUENCEVERIFY: 1 byte + // - OP_DATA: 1 byte (cltv_expiry length) + // - cltv_expiry: 4 bytes + // - OP_CHECKLOCKTIMEVERIFY: 1 byte + // - OP_DROP: 1 byte + TaprootHtlcOfferedRemoteTimeoutScriptSize = 1 + 32 + 1 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + + // TaprootHtlcOfferedRemoteTimeoutwitSize: 176 bytes + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - script_len: 1 byte + // - taproot_offered_htlc_script_size: 42 bytes + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + // - sibilng_merkle_proof: 32 bytes + TaprootHtlcOfferedRemoteTimeoutWitnessSize = 1 + 1 + 65 + 1 + + TaprootHtlcOfferedRemoteTimeoutScriptSize + 1 + + TaprootBaseControlBlockWitnessSize + 32 + + // TaprootHtlcOfferedLocalTmeoutScriptSize: + // - OP_DATA: 1 byte (pub key len) + // - local_key: 32 bytes + // - OP_CHECKSIGVERIFY: 1 byte + // - OP_DATA: 1 byte (pub key len) + // - remote_key: 32 bytes + // - OP_CHECKSIG: 1 byte + TaprootHtlcOfferedLocalTimeoutScriptSize = 1 + 32 + 1 + 1 + 32 + 1 + + // TaprootOfferedLocalTimeoutWitnessSize + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - script_len: 1 byte + // - taproot_offered_htlc_script_timeout_size: + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + // - sibilng_merkle_proof: 32 bytes + TaprootOfferedLocalTimeoutWitnessSize = 1 + 1 + 65 + 1 + 65 + 1 + + TaprootHtlcOfferedLocalTimeoutScriptSize + 1 + + TaprootBaseControlBlockWitnessSize + 32 + + // TaprootHtlcAcceptedRemoteSuccessScriptSize: + // - OP_SIZE: 1 byte + // - OP_DATA: 1 byte + // - 32: 1 byte + // - OP_EQUALVERIFY: 1 byte + // - OP_HASH160: 1 byte + // - OP_DATA: 1 byte (RIPEMD160(payment_hash) length) + // - RIPEMD160(payment_hash): 20 bytes + // - OP_EQUALVERIFY: 1 byte + // - OP_DATA: 1 byte (pub key len) + // - remote_key: 32 bytes + // - OP_CHECKSIG: 1 byte + // - OP_1: 1 byte + // - OP_CSV: 1 byte + // - OP_DROP: 1 byte + TaprootHtlcAcceptedRemoteSuccessScriptSize = 1 + 1 + 1 + 1 + 1 + 1 + + 1 + 20 + 1 + 32 + 1 + 1 + 1 + 1 + + // TaprootHtlcAcceptedRemoteSuccessScriptSize: + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - payment_preimage_length: 1 byte + // - payment_preimage: 32 bytes + // - script_len: 1 byte + // - taproot_offered_htlc_script_success_size: + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + // - sibilng_merkle_proof: 32 bytes + TaprootHtlcAcceptedRemoteSuccessWitnessSize = 1 + 1 + 65 + 1 + 32 + 1 + + TaprootHtlcAcceptedRemoteSuccessScriptSize + 1 + + TaprootBaseControlBlockWitnessSize + 32 + + // TaprootHtlcAcceptedLocalSuccessScriptSize: + // - OP_SIZE: 1 byte + // - OP_DATA: 1 byte + // - 32: 1 byte + // - OP_EQUALVERIFY: 1 byte + // - OP_HASH160: 1 byte + // - OP_DATA: 1 byte (RIPEMD160(payment_hash) length) + // - RIPEMD160(payment_hash): 20 bytes + // - OP_EQUALVERIFY: 1 byte + // - OP_DATA: 1 byte (pub key len) + // - local_key: 32 bytes + // - OP_CHECKSIGVERIFY: 1 byte + // - OP_DATA: 1 byte (pub key len) + // - remote_key: 32 bytes + // - OP_CHECKSIG: 1 byte + TaprootHtlcAcceptedLocalSuccessScriptSize = 1 + 1 + 1 + 1 + 1 + 1 + + 20 + 1 + 1 + 32 + 1 + 1 + 32 + 1 + + // TaprootHtlcAcceptedLocalSuccessWitnessSize: + // - number_of_witness_elements: 1 byte + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - sig_len: 1 byte + // - sweep_sig: 65 bytes (worst case w/o sighash default) + // - payment_preimage_length: 1 byte + // - payment_preimage: 32 bytes + // - script_len: 1 byte + // - taproot_accepted_htlc_script_success_size: + // - ctrl_block_len: 1 byte + // - base_control_block_size: 33 bytes + // - sibilng_merkle_proof: 32 bytes + TaprootHtlcAcceptedLocalSuccessWitnessSize = 1 + 1 + 65 + 1 + 65 + 1 + + 32 + 1 + TaprootHtlcAcceptedLocalSuccessScriptSize + 1 + + TaprootBaseControlBlockWitnessSize + 32 ) // EstimateCommitTxWeight estimate commitment transaction weight depending on @@ -572,6 +838,8 @@ func EstimateCommitTxWeight(count int, prediction bool) int64 { baseWeight := int64(BaseCommitmentTxWeight) witnessWeight := int64(WitnessCommitmentTxWeight) + // TODO(roasbeef): need taproot modifier? also no anchor so wrong? + return htlcWeight + baseWeight + witnessWeight } diff --git a/input/size_test.go b/input/size_test.go index be7e33459..b8d819f29 100644 --- a/input/size_test.go +++ b/input/size_test.go @@ -26,6 +26,10 @@ const ( // without the trailing sighash flag. maxDERSignatureSize = 72 + // maxSchnorrSignature is the largest possilbe schnorr sig w/o a non + // default sighash. + maxSchnorrSignatureSize = 64 + testAmt = btcutil.MaxSatoshi ) @@ -313,6 +317,18 @@ func (s *maxDERSignature) Verify(_ []byte, _ *btcec.PublicKey) bool { return true } +type maxSchnorrSignature struct{} + +func (s *maxSchnorrSignature) Serialize() []byte { + // Always return worst-case signature length, including a non-default + // sighash type. + return make([]byte, maxSchnorrSignatureSize) +} + +func (s *maxSchnorrSignature) Verify(_ []byte, _ *btcec.PublicKey) bool { + return true +} + // dummySigner is a fake signer used for size (upper bound) calculations. type dummySigner struct { input.Signer @@ -323,6 +339,15 @@ type dummySigner struct { func (s *dummySigner) SignOutputRaw(tx *wire.MsgTx, signDesc *input.SignDescriptor) (input.Signature, error) { + switch signDesc.SignMethod { + case input.TaprootKeySpendBIP0086SignMethod: + fallthrough + case input.TaprootKeySpendSignMethod: + fallthrough + case input.TaprootScriptSpendSignMethod: + return &maxSchnorrSignature{}, nil + } + return &maxDERSignature{}, nil } @@ -813,6 +838,494 @@ var witnessSizeTests = []witnessSizeTest{ t.Fatal(err) } + return witness + }, + }, + { + name: "taproot to local sweep", + expSize: input.TaprootToLocalWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + testKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + signer := &dummySigner{} + commitScriptTree, err := input.NewLocalCommitScriptTree( + testCSVDelay, testKey.PubKey(), testKey.PubKey(), + ) + require.NoError(t, err) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: testKey.PubKey(), + }, + WitnessScript: commitScriptTree.SettleLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.TaprootCommitSpendSuccess( + signer, signDesc, testTx, + commitScriptTree.TapscriptTree, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot to local revocation", + expSize: input.TaprootToLocalRevokeWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + testKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + signer := &dummySigner{} + commitScriptTree, err := input.NewLocalCommitScriptTree( + testCSVDelay, testKey.PubKey(), testKey.PubKey(), + ) + require.NoError(t, err) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: testKey.PubKey(), + }, + WitnessScript: commitScriptTree.RevocationLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.TaprootCommitSpendRevoke( + signer, signDesc, testTx, + commitScriptTree.TapscriptTree, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot to remote sweep", + expSize: input.TaprootToRemoteWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + testKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + signer := &dummySigner{} + commitScriptTree, err := input.NewRemoteCommitScriptTree( + testKey.PubKey(), + ) + require.NoError(t, err) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: testKey.PubKey(), + }, + WitnessScript: commitScriptTree.SettleLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.TaprootCommitRemoteSpend( + signer, signDesc, testTx, + commitScriptTree.TapscriptTree, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot anchor sweep", + expSize: input.TaprootAnchorWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + testKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + signer := &dummySigner{} + + anchorScriptTree, err := input.NewAnchorScriptTree( + testKey.PubKey(), + ) + require.NoError(t, err) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: testKey.PubKey(), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootKeySpendSignMethod, + TapTweak: anchorScriptTree.TapscriptRoot, + } + + witness, err := input.TaprootAnchorSpend( + signer, signDesc, testTx, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot second level htlc success+timeout", + expSize: input.TaprootSecondLevelHtlcWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + testKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + signer := &dummySigner{} + + scriptTree, err := input.SecondLevelHtlcTapscriptTree( + testKey.PubKey(), testCSVDelay, + ) + require.NoError(t, err) + + tapScriptRoot := scriptTree.RootNode.TapHash() + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + tapLeaf := scriptTree.LeafMerkleProofs[0].TapLeaf + witnessScript := tapLeaf.Script + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + WitnessScript: witnessScript, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootKeySpendSignMethod, + TapTweak: tapScriptRoot[:], + } + + witness, err := input.TaprootHtlcSpendSuccess( + signer, signDesc, revokeKey.PubKey(), testTx, + scriptTree, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot second level htlc revoke", + expSize: input.TaprootSecondLevelRevokeWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + testKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + scriptTree, err := input.SecondLevelHtlcTapscriptTree( + testKey.PubKey(), testCSVDelay, + ) + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + signer := &dummySigner{} + + tapScriptRoot := scriptTree.RootNode.TapHash() + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootKeySpendSignMethod, + TapTweak: tapScriptRoot[:], + } + + witness, err := input.TaprootHtlcSpendRevoke( + signer, signDesc, testTx, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot offered htlc revoke", + expSize: input.TaprootOfferedRevokeWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var payHash [32]byte + + signer := &dummySigner{} + + htlcScriptTree, err := input.SenderHTLCScriptTaproot( + senderKey.PubKey(), receiverKey.PubKey(), + revokeKey.PubKey(), payHash[:], + ) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootKeySpendSignMethod, + TapTweak: htlcScriptTree.TapscriptRoot, + } + + witness, err := input.SenderHTLCScriptTaprootRevoke( + signer, signDesc, testTx, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot accepted htlc revoke", + expSize: input.TaprootAcceptedRevokeWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var payHash [32]byte + + signer := &dummySigner{} + + htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( + testCLTVExpiry, senderKey.PubKey(), + receiverKey.PubKey(), revokeKey.PubKey(), + payHash[:], + ) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootKeySpendSignMethod, + TapTweak: htlcScriptTree.TapscriptRoot, + } + + witness, err := input.ReceiverHTLCScriptTaprootRevoke( + signer, signDesc, testTx, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot offered remote timeout", + expSize: input.TaprootHtlcOfferedRemoteTimeoutWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var payHash [32]byte + + signer := &dummySigner{} + + htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( + testCLTVExpiry, senderKey.PubKey(), + receiverKey.PubKey(), revokeKey.PubKey(), + payHash[:], + ) + + timeoutLeaf := htlcScriptTree.TimeoutTapLeaf + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: senderKey.PubKey(), + }, + WitnessScript: timeoutLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.ReceiverHTLCScriptTaprootTimeout( + signer, signDesc, testTx, testCLTVExpiry, + revokeKey.PubKey(), htlcScriptTree.TapscriptTree, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot offered local timeout", + expSize: input.TaprootOfferedLocalTimeoutWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var payHash [32]byte + + signer := &dummySigner{} + + htlcScriptTree, err := input.SenderHTLCScriptTaproot( + senderKey.PubKey(), receiverKey.PubKey(), + revokeKey.PubKey(), payHash[:], + ) + require.NoError(t, err) + + timeoutLeaf := htlcScriptTree.TimeoutTapLeaf + scriptTree := htlcScriptTree.TapscriptTree + + receiverDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: receiverKey.PubKey(), + }, + WitnessScript: timeoutLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + receiverSig, err := signer.SignOutputRaw( + testTx, receiverDesc, + ) + require.NoError(t, err) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: senderKey.PubKey(), + }, + WitnessScript: timeoutLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.SenderHTLCScriptTaprootTimeout( + receiverSig, txscript.SigHashAll, signer, + signDesc, testTx, revokeKey.PubKey(), + scriptTree, + ) + require.NoError(t, err) + return witness + }, + }, + { + name: "taproot accepted remote success", + expSize: input.TaprootHtlcAcceptedRemoteSuccessWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var payHash [32]byte + + signer := &dummySigner{} + + htlcScriptTree, err := input.SenderHTLCScriptTaproot( + senderKey.PubKey(), receiverKey.PubKey(), + revokeKey.PubKey(), + payHash[:], + ) + require.NoError(t, err) + + successLeaf := htlcScriptTree.SuccessTapLeaf + scriptTree := htlcScriptTree.TapscriptTree + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: receiverKey.PubKey(), + }, + WitnessScript: successLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.SenderHTLCScriptTaprootRedeem( + signer, signDesc, testTx, testPreimage, + revokeKey.PubKey(), scriptTree, + ) + require.NoError(t, err) + + return witness + }, + }, + { + name: "taproot accepted local success", + expSize: input.TaprootHtlcAcceptedLocalSuccessWitnessSize, + genWitness: func(t *testing.T) wire.TxWitness { + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var payHash [32]byte + + signer := &dummySigner{} + + htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( + testCLTVExpiry, senderKey.PubKey(), + receiverKey.PubKey(), revokeKey.PubKey(), + payHash[:], + ) + + successsLeaf := htlcScriptTree.SuccessTapLeaf + scriptTree := htlcScriptTree.TapscriptTree + + senderDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: senderKey.PubKey(), + }, + WitnessScript: successsLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + senderSig, err := signer.SignOutputRaw( + testTx, senderDesc, + ) + require.NoError(t, err) + + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: receiverKey.PubKey(), + }, + WitnessScript: successsLeaf.Script, + HashType: txscript.SigHashAll, + InputIndex: 0, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + witness, err := input.ReceiverHTLCScriptTaprootRedeem( + senderSig, txscript.SigHashAll, testPreimage, + signer, signDesc, testTx, revokeKey.PubKey(), + scriptTree, + ) + require.NoError(t, err) + return witness }, }, @@ -837,24 +1350,40 @@ func TestWitnessSizes(t *testing.T) { } // genTimeoutTx creates a signed HTLC second level timeout tx. -func genTimeoutTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { +func genTimeoutTx(t *testing.T, + chanType channeldb.ChannelType) *wire.MsgTx { + + testKeyPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + testPubkey := testKeyPriv.PubKey() + // Create the unsigned timeout tx. timeoutTx, err := lnwallet.CreateHtlcTimeoutTx( chanType, false, testOutPoint, testAmt, testCLTVExpiry, testCSVDelay, 0, testPubkey, testPubkey, ) - if err != nil { - return nil, err - } + require.NoError(t, err) - // In order to sign the transcation, generate the script for the output + // In order to sign the transaction, generate the script for the output // it spends. - witScript, err := input.SenderHTLCScript( - testPubkey, testPubkey, testPubkey, testHash160, - chanType.HasAnchors(), + var ( + witScript []byte + tapscriptTree *input.HtlcScriptTree ) - if err != nil { - return nil, err + if chanType.IsTaproot() { + tapscriptTree, err = input.SenderHTLCScriptTaproot( + testPubkey, testPubkey, testPubkey, testHash160, + ) + require.NoError(t, err) + + witScript = tapscriptTree.TimeoutTapLeaf.Script + } else { + witScript, err = input.SenderHTLCScript( + testPubkey, testPubkey, testPubkey, testHash160, + chanType.HasAnchors(), + ) + require.NoError(t, err) } signDesc := &input.SignDescriptor{ @@ -864,39 +1393,66 @@ func genTimeoutTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { }, } - // Sign the timeout tx and add the witness. sigHashType := lnwallet.HtlcSigHashType(chanType) - timeoutWitness, err := input.SenderHtlcSpendTimeout( - &maxDERSignature{}, sigHashType, &dummySigner{}, - signDesc, timeoutTx, - ) - if err != nil { - return nil, err + + // Sign the timeout tx and add the witness. + var timeoutWitness [][]byte + + if chanType.IsTaproot() { + signDesc.SignMethod = input.TaprootScriptSpendSignMethod + + timeoutWitness, err = input.SenderHTLCScriptTaprootTimeout( + &maxSchnorrSignature{}, sigHashType, &dummySigner{}, + signDesc, timeoutTx, testPubkey, + tapscriptTree.TapscriptTree, + ) + require.NoError(t, err) + } else { + timeoutWitness, err = input.SenderHtlcSpendTimeout( + &maxDERSignature{}, sigHashType, &dummySigner{}, + signDesc, timeoutTx, + ) + require.NoError(t, err) } timeoutTx.TxIn[0].Witness = timeoutWitness - return timeoutTx, nil + return timeoutTx } // genSuccessTx creates a signed HTLC second level success tx. -func genSuccessTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { - // Create the unisgned success tx. +func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx { + testKeyPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + testPubkey := testKeyPriv.PubKey() + + // Create the unsigned success tx. successTx, err := lnwallet.CreateHtlcSuccessTx( chanType, false, testOutPoint, testAmt, testCSVDelay, 0, testPubkey, testPubkey, ) - if err != nil { - return nil, err - } + require.NoError(t, err) - // In order to sign the transcation, generate the script for the output + // In order to sign the transaction, generate the script for the output // it spends. - witScript, err := input.ReceiverHTLCScript( - testCLTVExpiry, testPubkey, testPubkey, - testPubkey, testHash160, chanType.HasAnchors(), + var ( + witScript []byte + tapscriptTree *input.HtlcScriptTree ) - if err != nil { - return nil, err + if chanType.IsTaproot() { + tapscriptTree, err = input.ReceiverHTLCScriptTaproot( + testCLTVExpiry, testPubkey, testPubkey, testPubkey, + testHash160, + ) + require.NoError(t, err) + + witScript = tapscriptTree.SuccessTapLeaf.Script + } else { + witScript, err = input.ReceiverHTLCScript( + testCLTVExpiry, testPubkey, testPubkey, + testPubkey, testHash160, chanType.HasAnchors(), + ) + require.NoError(t, err) } signDesc := &input.SignDescriptor{ @@ -906,18 +1462,31 @@ func genSuccessTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { }, } - // Sign the success tx and add the witness. sigHashType := lnwallet.HtlcSigHashType(channeldb.SingleFunderBit) - successWitness, err := input.ReceiverHtlcSpendRedeem( - &maxDERSignature{}, sigHashType, testPreimage, - &dummySigner{}, signDesc, successTx, - ) - if err != nil { - return nil, err + + var successWitness [][]byte + + // Sign the success tx and add the witness. + if chanType.IsTaproot() { + signDesc.SignMethod = input.TaprootScriptSpendSignMethod + + successWitness, err = input.ReceiverHTLCScriptTaprootRedeem( + &maxSchnorrSignature{}, sigHashType, testPreimage, + &dummySigner{}, signDesc, successTx, testPubkey, + tapscriptTree.TapscriptTree, + ) + require.NoError(t, err) + } else { + successWitness, err = input.ReceiverHtlcSpendRedeem( + &maxDERSignature{}, sigHashType, testPreimage, + &dummySigner{}, signDesc, successTx, + ) + require.NoError(t, err) } + successTx.TxIn[0].Witness = successWitness - return successTx, nil + return successTx } type txSizeTest struct { @@ -928,36 +1497,35 @@ type txSizeTest struct { var txSizeTests = []txSizeTest{ { - name: "htlc timeout regular ", + name: "htlc timeout regular", expWeight: input.HtlcTimeoutWeight, genTx: func(t *testing.T) *wire.MsgTx { - tx, err := genTimeoutTx(channeldb.SingleFunderBit) - require.NoError(t, err) - - return tx + return genTimeoutTx(t, channeldb.SingleFunderBit) }, }, { name: "htlc timeout confirmed", expWeight: input.HtlcTimeoutWeightConfirmed, genTx: func(t *testing.T) *wire.MsgTx { - tx, err := genTimeoutTx(channeldb.AnchorOutputsBit) - require.NoError(t, err) - - return tx + return genTimeoutTx(t, channeldb.AnchorOutputsBit) + }, + }, + { + name: "taproot htlc timeout", + expWeight: input.TaprootHtlcTimeoutWeight, + genTx: func(t *testing.T) *wire.MsgTx { + return genTimeoutTx( + t, channeldb.SimpleTaprootFeatureBit, + ) }, }, - { name: "htlc success regular", // The weight estimate from the spec is off by one, but it's // okay since we overestimate the weight. expWeight: input.HtlcSuccessWeight - 1, genTx: func(t *testing.T) *wire.MsgTx { - tx, err := genSuccessTx(channeldb.SingleFunderBit) - require.NoError(t, err) - - return tx + return genSuccessTx(t, channeldb.SingleFunderBit) }, }, { @@ -966,22 +1534,30 @@ var txSizeTests = []txSizeTest{ // okay since we overestimate the weight. expWeight: input.HtlcSuccessWeightConfirmed - 1, genTx: func(t *testing.T) *wire.MsgTx { - tx, err := genSuccessTx(channeldb.AnchorOutputsBit) - require.NoError(t, err) - - return tx + return genSuccessTx(t, channeldb.AnchorOutputsBit) + }, + }, + { + name: "taproot htlc success", + expWeight: input.TaprootHtlcSuccessWeight, + genTx: func(t *testing.T) *wire.MsgTx { + return genSuccessTx( + t, channeldb.SimpleTaprootFeatureBit, + ) }, }, } -// TestWitnessSizes asserts the correctness of our magic tx size constants. +// TestTxSizes asserts the correctness of our magic tx size constants. func TestTxSizes(t *testing.T) { for _, test := range txSizeTests { test := test t.Run(test.name, func(t *testing.T) { tx := test.genTx(t) - weight := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) + weight := blockchain.GetTransactionWeight( + btcutil.NewTx(tx), + ) if weight != test.expWeight { t.Fatalf("size mismatch, want: %v, got: %v", test.expWeight, weight) diff --git a/input/taproot_test.go b/input/taproot_test.go index 6263e3aa0..b9e4b28be 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -224,6 +224,8 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { testCases := []struct { name string + // TODO(roasbeef): use sighash slice + witnessGen witnessGen txInMutator func(txIn *wire.TxIn) @@ -437,7 +439,7 @@ type testReceiverHtlcScriptTree struct { receiverKey *btcec.PrivateKey - revokeKey *btcec.PrivateKey + revokeKey btcec.PrivateKey htlcTxOut *wire.TxOut @@ -486,7 +488,7 @@ func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { preImage: preImage, senderKey: senderKey, receiverKey: receiverKey, - revokeKey: revokeKey, + revokeKey: *revokeKey, htlcTxOut: targetTxOut, htlcAmt: htlcAmt, rootHash: htlcScriptTree.TapscriptRoot, @@ -542,7 +544,7 @@ func htlcReceiverRevocationWitnessGen(sigHash txscript.SigHashType, revokeKey := htlcScriptTree.revokeKey signer := &MockSigner{ Privkeys: []*btcec.PrivateKey{ - revokeKey, + &revokeKey, }, } @@ -637,6 +639,8 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) { // signing below. htlcScriptTree := newTestReceiverHtlcScriptTree(t) + // TODO(roasbeef): issue with revoke key??? ctrl block even/odd + spendTx := wire.NewMsgTx(2) spendTx.AddTxIn(&wire.TxIn{}) spendTx.AddTxOut(&wire.TxOut{ diff --git a/input/witnessgen.go b/input/witnessgen.go index ad2735195..72c518d9b 100644 --- a/input/witnessgen.go +++ b/input/witnessgen.go @@ -206,7 +206,7 @@ const ( // TaprootHtlcAcceptedSuccessSecondLevel is a witness that allows us to // sweep an HTLC we accepted on our commitment transaction after we go // to the second level on chain. - TaprootHtlcAcceptedSuceessSecondLevel StandardWitnessType = 26 + TaprootHtlcAcceptedSuccessSecondLevel StandardWitnessType = 26 // TaprootHtlcSecondLevelRevoke is a witness that allows us to sweep an // HTLC on the revoked transaction of the remote party that goes to the @@ -339,8 +339,8 @@ func (wt StandardWitnessType) String() string { case TaprootHtlcOfferedTimeoutSecondLevel: return "TaprootHtlcOfferedTimeoutSecondLevel" - case TaprootHtlcAcceptedSuceessSecondLevel: - return "TaprootHtlcAcceptedSuceessSecondLevel" + case TaprootHtlcAcceptedSuccessSecondLevel: + return "TaprootHtlcAcceptedSuccessSecondLevel" case TaprootHtlcSecondLevelRevoke: return "TaprootHtlcSecondLevelRevoke" @@ -621,6 +621,48 @@ func (wt StandardWitnessType) SizeUpperBound() (int, bool, error) { case TaprootPubKeySpend: return TaprootKeyPathCustomSighashWitnessSize, false, nil + + // Sweeping a self output after a delay for taproot channels. + case TaprootLocalCommitSpend: + return TaprootToLocalWitnessSize, false, nil + + // Sweeping a self output after the remote party fro ce closes. Must + // wait 1 CSV. + case TaprootRemoteCommitSpend: + return TaprootToRemoteWitnessSize, false, nil + + // Sweeping our anchor output with a key spend witness. + case TaprootAnchorSweepSpend: + return TaprootAnchorWitnessSize, false, nil + + case TaprootHtlcOfferedTimeoutSecondLevel, + TaprootHtlcAcceptedSuccessSecondLevel: + + return TaprootSecondLevelHtlcWitnessSize, false, nil + + case TaprootHtlcSecondLevelRevoke: + return TaprootSecondLevelRevokeWitnessSize, false, nil + + case TaprootHtlcAcceptedRevoke: + return TaprootAcceptedRevokeWitnessSize, false, nil + + case TaprootHtlcOfferedRevoke: + return TaprootOfferedRevokeWitnessSize, false, nil + + case TaprootHtlcOfferedRemoteTimeout: + return TaprootHtlcOfferedRemoteTimeoutWitnessSize, false, nil + + case TaprootHtlcLocalOfferedTimeout: + return TaprootOfferedLocalTimeoutWitnessSize, false, nil + + case TaprootHtlcAcceptedRemoteSuccess: + return TaprootHtlcAcceptedRemoteSuccessWitnessSize, false, nil + + case TaprootHtlcAcceptedLocalSuccess: + return TaprootHtlcAcceptedLocalSuccessWitnessSize, false, nil + + case TaprootCommitmentRevoke: + return TaprootToLocalRevokeWitnessSize, false, nil } return 0, false, fmt.Errorf("unexpected witness type: %v", wt) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index f4b3a14af..97e2d2ea2 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -242,7 +242,9 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // Based on the channel type, we determine the initial commit weight // and fee. commitWeight := int64(input.CommitWeight) - if req.CommitType.HasAnchors() { + if req.CommitType.IsTaproot() { + commitWeight = input.TaprootCommitWeight + } else if req.CommitType.HasAnchors() { commitWeight = int64(input.AnchorCommitWeight) } commitFee := req.CommitFeePerKw.FeeForWeight(commitWeight)