From 008569f80498d6bf5949d619b8bd53520c7a82ee Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Thu, 29 Aug 2024 11:11:51 +0200 Subject: [PATCH] Add withdrawal command --- cmd/stakercli/transaction/transactions.go | 342 +++++++++++++++++- .../transaction/transactions_test.go | 211 +++++++++++ 2 files changed, 543 insertions(+), 10 deletions(-) diff --git a/cmd/stakercli/transaction/transactions.go b/cmd/stakercli/transaction/transactions.go index c4b194b..ae3be27 100644 --- a/cmd/stakercli/transaction/transactions.go +++ b/cmd/stakercli/transaction/transactions.go @@ -1,6 +1,7 @@ package transaction import ( + "bytes" "encoding/hex" "errors" "fmt" @@ -14,6 +15,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/cometbft/cometbft/libs/os" @@ -24,16 +26,19 @@ import ( ) const ( - stakingTransactionFlag = "staking-transaction" - networkNameFlag = "network" - stakerPublicKeyFlag = "staker-pk" - finalityProviderKeyFlag = "finality-provider-pk" - txInclusionHeightFlag = "tx-inclusion-height" - tagFlag = "tag" - covenantMembersPksFlag = "covenant-committee-pks" - covenantQuorumFlag = "covenant-quorum" - minStakingAmountFlag = "min-staking-amount" - maxStakingAmountFlag = "max-staking-amount" + stakingTransactionFlag = "staking-transaction" + unbondingTransactionFlag = "unbonding-transaction" + networkNameFlag = "network" + stakerPublicKeyFlag = "staker-pk" + finalityProviderKeyFlag = "finality-provider-pk" + txInclusionHeightFlag = "tx-inclusion-height" + tagFlag = "tag" + covenantMembersPksFlag = "covenant-committee-pks" + covenantQuorumFlag = "covenant-quorum" + minStakingAmountFlag = "min-staking-amount" + maxStakingAmountFlag = "max-staking-amount" + withdrawalAddressFlag = "withdrawal-address" + withdrawalTransactionFeeFlag = "withdrawal-fee" ) var TransactionCommands = []cli.Command{ @@ -48,6 +53,7 @@ var TransactionCommands = []cli.Command{ checkPhase1StakingTransactionParamsCmd, createPhase1StakingTransactionWithParamsCmd, createPhase1UnbondingTransactionCmd, + createPhase1WithdrawalTransactionCmd, }, }, } @@ -786,3 +792,319 @@ func createPhase1UnbondingTransaction(ctx *cli.Context) error { helpers.PrintRespJSON(resp) return nil } + +type withdrawalInfo struct { + withdrawalOutputvalue btcutil.Amount + withdrawalSequence uint32 + withdrawalInput *wire.OutPoint + withdrawalFundingUtxo *wire.TxOut + withdrawalSpendInfo *btcstaking.SpendInfo +} + +func outputsAreEqual(a *wire.TxOut, b *wire.TxOut) bool { + return a.Value == b.Value && bytes.Equal(a.PkScript, b.PkScript) +} + +// createPhase1WithdrawalTransactionCmd creates un-signed withdrawal transaction based on +// provided valid phase1 staking transaction or valid unbonding transaction. +var createPhase1WithdrawalTransactionCmd = cli.Command{ + Name: "create-phase1-withdrawal-transaction", + ShortName: "crpwt", + Usage: "stakercli transaction create-phase1-withdrawal-transaction [fullpath/to/parameters.json]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: stakingTransactionFlag, + Usage: "original hex encoded staking transaction", + Required: true, + }, + cli.Uint64Flag{ + Name: txInclusionHeightFlag, + Usage: "Inclusion height of the staking transaction. Necessary to chose correct global parameters for transaction", + Required: true, + }, + cli.StringFlag{ + Name: withdrawalAddressFlag, + Usage: "btc address to which send the withdrawed funds", + Required: true, + }, + cli.Int64Flag{ + Name: withdrawalTransactionFeeFlag, + Usage: "fee to pay for withdrawal transaction", + Required: true, + }, + cli.StringFlag{ + Name: networkNameFlag, + Usage: "Bitcoin network on which withdrawal should take place one of (mainnet, testnet3, regtest, simnet, signet)", + Required: true, + }, + cli.StringFlag{ + Name: unbondingTransactionFlag, + Usage: "hex encoded unbonding transaction. This should only be provided, if withdrawal is being done from unbonding output", + }, + }, + Action: createPhase1WitdrawalTransaction, +} + +type CreateWithdrawalTxResponse struct { + // bare hex of created withdrawal transaction + WithdrawalTxHex string `json:"withdrawal_tx_hex"` + // base64 encoded psbt packet which can be used to sign the transaction using + // staker bitcoind wallet using `walletprocesspsbt` rpc call + WithdrawalPsbtPacketBase64 string `json:"withdrawal_psbt_packet_base64"` +} + +func createWithdrawalInfo( + unbondingTxHex string, + stakingTxHash *chainhash.Hash, + withdrawalFee btcutil.Amount, + parsedStakingTransaction *btcstaking.ParsedV0StakingTx, + paramsForHeight *parser.ParsedVersionedGlobalParams, + net *chaincfg.Params) (*withdrawalInfo, error) { + + if len(unbondingTxHex) > 0 { + // withdrawal from unbonding output + unbondingTx, _, err := bbn.NewBTCTxFromHex(unbondingTxHex) + + if err != nil { + return nil, fmt.Errorf("error parsing unbonding transaction: %w", err) + } + + unbondingTxHash := unbondingTx.TxHash() + + if err := btcstaking.IsSimpleTransfer(unbondingTx); err != nil { + return nil, fmt.Errorf("unbonding transaction is not valid: %w", err) + } + + if !unbondingTx.TxIn[0].PreviousOutPoint.Hash.IsEqual(stakingTxHash) { + return nil, fmt.Errorf("unbonding transaction does not spend staking transaction hash") + } + + if unbondingTx.TxIn[0].PreviousOutPoint.Index != uint32(parsedStakingTransaction.StakingOutputIdx) { + return nil, fmt.Errorf("unbonding transaction does not spend staking transaction index") + } + + expectedUnbondingAmount := parsedStakingTransaction.StakingOutput.Value - int64(paramsForHeight.UnbondingFee) + + if expectedUnbondingAmount <= 0 { + return nil, fmt.Errorf("too low staking output value to create unbonding transaction. Staking amount: %d, Unbonding fee: %d", parsedStakingTransaction.StakingOutput.Value, paramsForHeight.UnbondingFee) + } + + unbondingInfo, err := btcstaking.BuildUnbondingInfo( + parsedStakingTransaction.OpReturnData.StakerPublicKey.PubKey, + []*btcec.PublicKey{parsedStakingTransaction.OpReturnData.FinalityProviderPublicKey.PubKey}, + paramsForHeight.CovenantPks, + paramsForHeight.CovenantQuorum, + paramsForHeight.UnbondingTime, + btcutil.Amount(expectedUnbondingAmount), + net, + ) + + if err != nil { + return nil, fmt.Errorf("error building unbonding info: %w", err) + } + + if !outputsAreEqual(unbondingInfo.UnbondingOutput, unbondingTx.TxOut[0]) { + return nil, fmt.Errorf("unbonding transaction output does not match with expected output") + } + + timeLockPathInfo, err := unbondingInfo.TimeLockPathSpendInfo() + + if err != nil { + return nil, fmt.Errorf("error building time lock path spend info: %w", err) + } + + withdrawalOutputValue := unbondingTx.TxOut[0].Value - int64(withdrawalFee) + + if withdrawalOutputValue <= 0 { + return nil, fmt.Errorf("too low unbonding output value to create withdrawal transaction. Unbonding amount: %d, Withdrawal fee: %d", unbondingTx.TxOut[0].Value, withdrawalFee) + } + + return &withdrawalInfo{ + withdrawalOutputvalue: btcutil.Amount(withdrawalOutputValue), + withdrawalSequence: uint32(paramsForHeight.UnbondingTime), + withdrawalInput: wire.NewOutPoint(&unbondingTxHash, 0), + withdrawalFundingUtxo: unbondingTx.TxOut[0], + withdrawalSpendInfo: timeLockPathInfo, + }, nil + } else { + stakingInfo, err := btcstaking.BuildStakingInfo( + parsedStakingTransaction.OpReturnData.StakerPublicKey.PubKey, + []*btcec.PublicKey{parsedStakingTransaction.OpReturnData.FinalityProviderPublicKey.PubKey}, + paramsForHeight.CovenantPks, + paramsForHeight.CovenantQuorum, + parsedStakingTransaction.OpReturnData.StakingTime, + btcutil.Amount(parsedStakingTransaction.StakingOutput.Value), + net, + ) + + if err != nil { + return nil, fmt.Errorf("error building staking info: %w", err) + } + + timelockPathInfo, err := stakingInfo.TimeLockPathSpendInfo() + + if err != nil { + return nil, fmt.Errorf("error building timelock path spend info: %w", err) + } + + withdrawalOutputValue := parsedStakingTransaction.StakingOutput.Value - int64(withdrawalFee) + + if withdrawalOutputValue <= 0 { + return nil, fmt.Errorf("too low staking output value to create withdrawal transaction. Staking amount: %d, Withdrawal fee: %d", parsedStakingTransaction.StakingOutput.Value, withdrawalFee) + } + + return &withdrawalInfo{ + withdrawalOutputvalue: btcutil.Amount(withdrawalOutputValue), + withdrawalSequence: uint32(parsedStakingTransaction.OpReturnData.StakingTime), + withdrawalInput: wire.NewOutPoint(stakingTxHash, uint32(parsedStakingTransaction.StakingOutputIdx)), + withdrawalFundingUtxo: parsedStakingTransaction.StakingOutput, + withdrawalSpendInfo: timelockPathInfo, + }, nil + } +} + +func createPhase1WitdrawalTransaction(ctx *cli.Context) error { + inputFilePath := ctx.Args().First() + if len(inputFilePath) == 0 { + return errors.New("json file input is empty") + } + + if !os.FileExists(inputFilePath) { + return fmt.Errorf("json file input %s does not exist", inputFilePath) + } + + globalParams, err := parser.NewParsedGlobalParamsFromFile(inputFilePath) + + if err != nil { + return fmt.Errorf("error parsing file %s: %w", inputFilePath, err) + } + + net := ctx.String(networkNameFlag) + + currentParams, err := utils.GetBtcNetworkParams(net) + + if err != nil { + return err + } + + withdrawalFee, err := parseAmountFromCliCtx(ctx, withdrawalTransactionFeeFlag) + + if err != nil { + return err + } + + withdrawalAddressString := ctx.String(withdrawalAddressFlag) + + withdrawalAddress, err := btcutil.DecodeAddress(withdrawalAddressString, currentParams) + + if err != nil { + return fmt.Errorf("error decoding withdrawal address: %w", err) + } + + stakingTxHex := ctx.String(stakingTransactionFlag) + + stakingTx, _, err := bbn.NewBTCTxFromHex(stakingTxHex) + + if err != nil { + return err + } + + stakingTxInclusionHeight := ctx.Uint64(txInclusionHeightFlag) + + paramsForHeight := globalParams.GetVersionedGlobalParamsByHeight(stakingTxInclusionHeight) + + if paramsForHeight == nil { + return fmt.Errorf("no global params found for height %d", stakingTxInclusionHeight) + } + + parsedStakingTransaction, err := btcstaking.ParseV0StakingTx( + stakingTx, + paramsForHeight.Tag, + paramsForHeight.CovenantPks, + paramsForHeight.CovenantQuorum, + currentParams, + ) + + if err != nil { + return fmt.Errorf("provided staking transaction is not valid: %w, for params at height %d", err, stakingTxInclusionHeight) + } + + stakingTxHash := stakingTx.TxHash() + + unbondingTxHex := ctx.String(unbondingTransactionFlag) + + wi, err := createWithdrawalInfo( + unbondingTxHex, + &stakingTxHash, + withdrawalFee, + parsedStakingTransaction, + paramsForHeight, + currentParams, + ) + + if err != nil { + return err + } + + withdrawalPkScript, err := txscript.PayToAddrScript(withdrawalAddress) + + if err != nil { + return fmt.Errorf("error creating pk script for withdrawal address: %w", err) + } + + withdrawTxPsbPacket, err := psbt.New( + []*wire.OutPoint{wi.withdrawalInput}, + []*wire.TxOut{ + wire.NewTxOut(int64(wi.withdrawalOutputvalue), withdrawalPkScript), + }, + 2, + 0, + []uint32{wi.withdrawalSequence}, + ) + + if err != nil { + return err + } + + serializedControlBlock, err := wi.withdrawalSpendInfo.ControlBlock.ToBytes() + + if err != nil { + return err + } + + // Fill psbt packet with data which will make it possible for staker to sign + // it using his bitcoind wallet + withdrawTxPsbPacket.Inputs[0].SighashType = txscript.SigHashDefault + withdrawTxPsbPacket.Inputs[0].WitnessUtxo = wi.withdrawalFundingUtxo + withdrawTxPsbPacket.Inputs[0].TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{ + { + XOnlyPubKey: parsedStakingTransaction.OpReturnData.StakerPublicKey.Marshall(), + }, + } + withdrawTxPsbPacket.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{ + { + ControlBlock: serializedControlBlock, + Script: wi.withdrawalSpendInfo.RevealedLeaf.Script, + LeafVersion: wi.withdrawalSpendInfo.RevealedLeaf.LeafVersion, + }, + } + + withdrawalTxBytes, err := utils.SerializeBtcTransaction(withdrawTxPsbPacket.UnsignedTx) + if err != nil { + return err + } + + encodedPsbtPacket, err := withdrawTxPsbPacket.B64Encode() + + if err != nil { + return err + } + + resp := &CreateWithdrawalTxResponse{ + WithdrawalTxHex: hex.EncodeToString(withdrawalTxBytes), + WithdrawalPsbtPacketBase64: encodedPsbtPacket, + } + + helpers.PrintRespJSON(resp) + return nil +} diff --git a/cmd/stakercli/transaction/transactions_test.go b/cmd/stakercli/transaction/transactions_test.go index 954c88b..a3f6251 100644 --- a/cmd/stakercli/transaction/transactions_test.go +++ b/cmd/stakercli/transaction/transactions_test.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" @@ -209,6 +210,17 @@ func appRunCreatePhase1UnbondingTx(r *rand.Rand, t *testing.T, app *cli.App, arg return data } +func appRunCreatePhase1WithdrawalTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreateWithdrawalTxResponse { + args := []string{"stakercli", "transaction", "create-phase1-withdrawal-transaction"} + args = append(args, arguments...) + output := appRunWithOutput(r, t, app, args) + + var data transaction.CreateWithdrawalTxResponse + err := json.Unmarshal([]byte(output), &data) + require.NoError(t, err) + return data +} + func randRange(r *rand.Rand, min, max int) int { return rand.Intn(max+1-min) + min } @@ -493,3 +505,202 @@ func FuzzCreateUnbondingTx(f *testing.F) { require.NotNil(t, decoded) }) } + +func FuzzCreateWithdrawalStaking(f *testing.F) { + paramsFilePath := createTempFileWithParams(f) + + datagen.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + + stakerParams, _ := createCustomValidStakeParams(t, r, &globalParams, &chaincfg.RegressionNetParams) + + info, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( + lastParams.Tag, + stakerParams.StakerPk, + stakerParams.FinalityProviderPk, + lastParams.CovenantPks, + lastParams.CovenantQuorum, + stakerParams.StakingTime, + stakerParams.StakingAmount, + &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + + fakeInputHash := sha256.Sum256([]byte{0x01}) + tx.AddTxIn(wire.NewTxIn(&wire.OutPoint{Hash: fakeInputHash, Index: 0}, nil, nil)) + + serializedStakingTx, err := utils.SerializeBtcTransaction(tx) + require.NoError(t, err) + + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + addr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(key.PubKey()), + &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + + fee := int64(100) + createTxCmdArgs := []string{ + paramsFilePath, + fmt.Sprintf("--staking-transaction=%s", hex.EncodeToString(serializedStakingTx)), + fmt.Sprintf("--tx-inclusion-height=%d", stakerParams.InclusionHeight), + fmt.Sprintf("--withdrawal-address=%s", addr.EncodeAddress()), + fmt.Sprintf("--withdrawal-fee=%d", fee), + fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), + } + + app := testApp() + wr := appRunCreatePhase1WithdrawalTx(r, t, app, createTxCmdArgs) + require.NotNil(t, wr) + + wtx, _, err := bbn.NewBTCTxFromHex(wr.WithdrawalTxHex) + require.NoError(t, err) + require.NotNil(t, wtx) + require.Len(t, wtx.TxOut, 1) + require.Len(t, wtx.TxIn, 1) + + stakingTxHash := tx.TxHash() + require.Equal(t, stakingTxHash, wtx.TxIn[0].PreviousOutPoint.Hash) + require.Equal(t, uint32(0), wtx.TxIn[0].PreviousOutPoint.Index) + require.Equal(t, uint32(stakerParams.StakingTime), wtx.TxIn[0].Sequence) + require.Equal(t, int32(2), tx.Version) + + addrPkScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + require.Equal(t, addrPkScript, wtx.TxOut[0].PkScript) + require.Equal(t, int64(stakerParams.StakingAmount)-fee, wtx.TxOut[0].Value) + + decodedBytes, err := base64.StdEncoding.DecodeString(wr.WithdrawalPsbtPacketBase64) + require.NoError(t, err) + require.NotNil(t, decodedBytes) + decoded, err := psbt.NewFromRawBytes(bytes.NewReader(decodedBytes), false) + require.NoError(t, err) + require.NotNil(t, decoded) + + require.Len(t, decoded.Inputs, 1) + require.Equal(t, info.StakingOutput, decoded.Inputs[0].WitnessUtxo) + require.Equal(t, schnorr.SerializePubKey(stakerParams.StakerPk), decoded.Inputs[0].TaprootBip32Derivation[0].XOnlyPubKey) + + tli, err := info.TimeLockPathSpendInfo() + require.NoError(t, err) + ctrlBlockBytes, err := tli.ControlBlock.ToBytes() + require.NoError(t, err) + require.Equal(t, ctrlBlockBytes, decoded.Inputs[0].TaprootLeafScript[0].ControlBlock) + require.Equal(t, tli.RevealedLeaf.Script, decoded.Inputs[0].TaprootLeafScript[0].Script) + require.Equal(t, tli.RevealedLeaf.LeafVersion, decoded.Inputs[0].TaprootLeafScript[0].LeafVersion) + + }) +} + +func FuzzCreateWithdrawalUnbonding(f *testing.F) { + paramsFilePath := createTempFileWithParams(f) + + datagen.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + + stakerParams, _ := createCustomValidStakeParams(t, r, &globalParams, &chaincfg.RegressionNetParams) + + _, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( + lastParams.Tag, + stakerParams.StakerPk, + stakerParams.FinalityProviderPk, + lastParams.CovenantPks, + lastParams.CovenantQuorum, + stakerParams.StakingTime, + stakerParams.StakingAmount, + &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + fakeInputHash := sha256.Sum256([]byte{0x01}) + tx.AddTxIn(wire.NewTxIn(&wire.OutPoint{Hash: fakeInputHash, Index: 0}, nil, nil)) + + stakingTxHash := tx.TxHash() + + unbondingInfo, err := btcstaking.BuildUnbondingInfo( + stakerParams.StakerPk, + []*btcec.PublicKey{stakerParams.FinalityProviderPk}, + lastParams.CovenantPks, + lastParams.CovenantQuorum, + lastParams.UnbondingTime, + stakerParams.StakingAmount-lastParams.UnbondingFee, + &chaincfg.RegressionNetParams, + ) + + require.NoError(t, err) + + unbondingTx := wire.NewMsgTx(2) + unbondingTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{Hash: stakingTxHash, Index: 0}, nil, nil)) + unbondingTx.AddTxOut(unbondingInfo.UnbondingOutput) + + serializedStakingTx, err := utils.SerializeBtcTransaction(tx) + require.NoError(t, err) + + unbondingTxHash := unbondingTx.TxHash() + + serializedUnbondingTx, err := utils.SerializeBtcTransaction(unbondingTx) + require.NoError(t, err) + + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + addr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(key.PubKey()), + &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + + fee := int64(100) + createTxCmdArgs := []string{ + paramsFilePath, + fmt.Sprintf("--staking-transaction=%s", hex.EncodeToString(serializedStakingTx)), + fmt.Sprintf("--tx-inclusion-height=%d", stakerParams.InclusionHeight), + fmt.Sprintf("--withdrawal-address=%s", addr.EncodeAddress()), + fmt.Sprintf("--withdrawal-fee=%d", fee), + fmt.Sprintf("--unbonding-transaction=%s", hex.EncodeToString(serializedUnbondingTx)), + fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), + } + + app := testApp() + wr := appRunCreatePhase1WithdrawalTx(r, t, app, createTxCmdArgs) + require.NotNil(t, wr) + + wtx, _, err := bbn.NewBTCTxFromHex(wr.WithdrawalTxHex) + require.NoError(t, err) + require.NotNil(t, wtx) + require.Len(t, wtx.TxOut, 1) + require.Len(t, wtx.TxIn, 1) + + require.Equal(t, unbondingTxHash, wtx.TxIn[0].PreviousOutPoint.Hash) + require.Equal(t, uint32(0), wtx.TxIn[0].PreviousOutPoint.Index) + require.Equal(t, uint32(lastParams.UnbondingTime), wtx.TxIn[0].Sequence) + require.Equal(t, int32(2), tx.Version) + + addrPkScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + require.Equal(t, addrPkScript, wtx.TxOut[0].PkScript) + require.Equal(t, int64(unbondingInfo.UnbondingOutput.Value)-fee, wtx.TxOut[0].Value) + + decodedBytes, err := base64.StdEncoding.DecodeString(wr.WithdrawalPsbtPacketBase64) + require.NoError(t, err) + require.NotNil(t, decodedBytes) + decoded, err := psbt.NewFromRawBytes(bytes.NewReader(decodedBytes), false) + require.NoError(t, err) + require.NotNil(t, decoded) + + require.Len(t, decoded.Inputs, 1) + require.Equal(t, unbondingInfo.UnbondingOutput, decoded.Inputs[0].WitnessUtxo) + require.Equal(t, schnorr.SerializePubKey(stakerParams.StakerPk), decoded.Inputs[0].TaprootBip32Derivation[0].XOnlyPubKey) + + tli, err := unbondingInfo.TimeLockPathSpendInfo() + require.NoError(t, err) + ctrlBlockBytes, err := tli.ControlBlock.ToBytes() + require.NoError(t, err) + require.Equal(t, ctrlBlockBytes, decoded.Inputs[0].TaprootLeafScript[0].ControlBlock) + require.Equal(t, tli.RevealedLeaf.Script, decoded.Inputs[0].TaprootLeafScript[0].Script) + require.Equal(t, tli.RevealedLeaf.LeafVersion, decoded.Inputs[0].TaprootLeafScript[0].LeafVersion) + }) +}