package btcwallet import ( "bytes" "crypto/sha256" "encoding/hex" "fmt" "testing" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "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/btcsuite/btcwallet/waddrmgr" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" "github.com/stretchr/testify/require" ) var ( netParams = &chaincfg.RegressionNetParams testValue int64 = 345678 testCSVTimeout uint32 = 2016 testCommitSecretBytes, _ = hex.DecodeString( "9f1f0db609718cf70c580aec6a0e570c3f086ec85a2a6119295b1d64240d" + "aca5", ) testCommitSecret, testCommitPoint = btcec.PrivKeyFromBytes( testCommitSecretBytes, ) remoteRevocationBasePubKeyBytes, _ = hex.DecodeString( "02baf067bfd1a6cf7229c7c459b106d384ad33e948ea1d561f2034475ff1" + "7359fb", ) remoteRevocationBasePubKey, _ = btcec.ParsePubKey( remoteRevocationBasePubKeyBytes, ) testTweakSingle, _ = hex.DecodeString( "020143a30cf6b71ca2af01efbd1758a04b4c7f5c2299f2ea63a8a6b58107" + "63b1ed", ) ) // testInputType is a type that represents different types of inputs that are // signed within a PSBT. type testInputType uint8 const ( plainP2WKH testInputType = 0 tweakedP2WKH testInputType = 1 nestedP2WKH testInputType = 2 singleKeyP2WSH testInputType = 3 singleKeyDoubleTweakedP2WSH testInputType = 4 ) func (i testInputType) keyPath() []uint32 { switch i { case nestedP2WKH: return []uint32{ hardenedKey(waddrmgr.KeyScopeBIP0049Plus.Purpose), hardenedKey(0), hardenedKey(0), 0, 0, } case singleKeyP2WSH: return []uint32{ hardenedKey(keychain.BIP0043Purpose), hardenedKey(netParams.HDCoinType), hardenedKey(uint32(keychain.KeyFamilyPaymentBase)), 0, 7, } case singleKeyDoubleTweakedP2WSH: return []uint32{ hardenedKey(keychain.BIP0043Purpose), hardenedKey(netParams.HDCoinType), hardenedKey(uint32(keychain.KeyFamilyDelayBase)), 0, 9, } default: return []uint32{ hardenedKey(waddrmgr.KeyScopeBIP0084.Purpose), hardenedKey(0), hardenedKey(0), 0, 0, } } } func (i testInputType) output(t *testing.T, privKey *btcec.PrivateKey) (*wire.TxOut, []byte) { var ( addr btcutil.Address witnessScript []byte err error ) switch i { case plainP2WKH: h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) addr, err = btcutil.NewAddressWitnessPubKeyHash(h, netParams) require.NoError(t, err) case tweakedP2WKH: privKey = input.TweakPrivKey(privKey, testTweakSingle) h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) addr, err = btcutil.NewAddressWitnessPubKeyHash(h, netParams) require.NoError(t, err) case nestedP2WKH: h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( h, netParams, ) require.NoError(t, err) witnessProgram, err := txscript.PayToAddrScript(witnessAddr) require.NoError(t, err) addr, err = btcutil.NewAddressScriptHash( witnessProgram, netParams, ) require.NoError(t, err) case singleKeyP2WSH: // We're simulating a delay-to-self script which we're going to // spend through the time lock path. We don't actually need to // know the private key of the remote revocation base key. revokeKey := input.DeriveRevocationPubkey( remoteRevocationBasePubKey, testCommitPoint, ) witnessScript, err = input.CommitScriptToSelf( testCSVTimeout, privKey.PubKey(), revokeKey, ) require.NoError(t, err) h := sha256.Sum256(witnessScript) addr, err = btcutil.NewAddressWitnessScriptHash(h[:], netParams) require.NoError(t, err) case singleKeyDoubleTweakedP2WSH: // We're simulating breaching a remote party's delay-to-self // output which we're going to spend through the revocation // path. In that case the self key is the other party's self key // and, we only know the revocation base private key and commit // secret. revokeKey := input.DeriveRevocationPubkey( privKey.PubKey(), testCommitPoint, ) witnessScript, err = input.CommitScriptToSelf( testCSVTimeout, remoteRevocationBasePubKey, revokeKey, ) require.NoError(t, err) h := sha256.Sum256(witnessScript) addr, err = btcutil.NewAddressWitnessScriptHash(h[:], netParams) require.NoError(t, err) default: t.Fatalf("invalid input type") } pkScript, err := txscript.PayToAddrScript(addr) require.NoError(t, err) return &wire.TxOut{ Value: testValue, PkScript: pkScript, }, witnessScript } func (i testInputType) decorateInput(t *testing.T, privKey *btcec.PrivateKey, in *psbt.PInput) { switch i { case tweakedP2WKH: in.Unknowns = []*psbt.Unknown{{ Key: PsbtKeyTypeInputSignatureTweakSingle, Value: testTweakSingle, }} case nestedP2WKH: h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( h, netParams, ) require.NoError(t, err) witnessProgram, err := txscript.PayToAddrScript(witnessAddr) require.NoError(t, err) in.RedeemScript = witnessProgram case singleKeyDoubleTweakedP2WSH: in.Unknowns = []*psbt.Unknown{{ Key: PsbtKeyTypeInputSignatureTweakDouble, Value: testCommitSecret.Serialize(), }} } } func (i testInputType) finalize(t *testing.T, packet *psbt.Packet) { in := &packet.Inputs[0] sigBytes := in.PartialSigs[0].Signature var witnessStack wire.TxWitness switch i { case singleKeyP2WSH: witnessStack = make([][]byte, 3) witnessStack[0] = sigBytes witnessStack[1] = nil witnessStack[2] = in.WitnessScript case singleKeyDoubleTweakedP2WSH: // Place a 1 as the first item in the evaluated witness stack to // force script execution to the revocation clause. witnessStack = make([][]byte, 3) witnessStack[0] = sigBytes witnessStack[1] = []byte{1} witnessStack[2] = in.WitnessScript default: // The PSBT finalizer knows what to do if we're not using a // custom script. err := psbt.MaybeFinalizeAll(packet) require.NoError(t, err) return } var err error in.FinalScriptWitness, err = serializeTxWitness(witnessStack) require.NoError(t, err) } // serializeTxWitness return the wire witness stack into raw bytes. func serializeTxWitness(txWitness wire.TxWitness) ([]byte, error) { var witnessBytes bytes.Buffer err := psbt.WriteTxWitness(&witnessBytes, txWitness) if err != nil { return nil, fmt.Errorf("error serializing witness: %w", err) } return witnessBytes.Bytes(), nil } // TestSignPsbt tests the PSBT signing functionality. func TestSignPsbt(t *testing.T) { w, _ := newTestWallet(t, netParams, seedBytes) testCases := []struct { name string inputType testInputType }{{ name: "plain P2WKH", inputType: plainP2WKH, }, { name: "tweaked P2WKH", inputType: tweakedP2WKH, }, { name: "nested P2WKH", inputType: nestedP2WKH, }, { name: "single key P2WSH", inputType: singleKeyP2WSH, }, { name: "single key double tweaked P2WSH", inputType: singleKeyDoubleTweakedP2WSH, }} for _, tc := range testCases { tc := tc // This is the private key we're going to sign with. privKey, err := w.deriveKeyByBIP32Path(tc.inputType.keyPath()) require.NoError(t, err) txOut, witnessScript := tc.inputType.output(t, privKey) // Create the reference transaction that has the input that is // going to be spent by our PSBT. refTx := wire.NewMsgTx(2) refTx.AddTxIn(&wire.TxIn{}) refTx.AddTxOut(txOut) // Create the unsigned spend transaction that is going to be the // main content of our PSBT. spendTx := wire.NewMsgTx(2) spendTx.LockTime = testCSVTimeout spendTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: wire.OutPoint{ Hash: refTx.TxHash(), Index: 0, }, Sequence: testCSVTimeout, }) spendTx.AddTxOut(txOut) // Convert it to a PSBT now and add all required signing // metadata to it. packet, err := psbt.NewFromUnsignedTx(spendTx) require.NoError(t, err) packet.Inputs[0].WitnessScript = witnessScript packet.Inputs[0].SighashType = txscript.SigHashAll packet.Inputs[0].WitnessUtxo = refTx.TxOut[0] packet.Inputs[0].Bip32Derivation = []*psbt.Bip32Derivation{{ PubKey: privKey.PubKey().SerializeCompressed(), Bip32Path: tc.inputType.keyPath(), }} tc.inputType.decorateInput(t, privKey, &packet.Inputs[0]) // Let the wallet do its job. We expect to be the only signer // for this PSBT, so we'll be able to finalize it later. signedInputs, err := w.SignPsbt(packet) require.NoError(t, err) // We expect one signed input at index 0. require.Len(t, signedInputs, 1) require.EqualValues(t, 0, signedInputs[0]) // If the witness stack needs to be assembled, give the caller // the option to do that now. tc.inputType.finalize(t, packet) finalTx, err := psbt.Extract(packet) require.NoError(t, err) vm, err := txscript.NewEngine( refTx.TxOut[0].PkScript, finalTx, 0, txscript.StandardVerifyFlags, nil, nil, refTx.TxOut[0].Value, txscript.NewCannedPrevOutputFetcher( refTx.TxOut[0].PkScript, refTx.TxOut[0].Value, ), ) require.NoError(t, err) require.NoError(t, vm.Execute()) } } // TestEstimateInputWeight tests that we correctly estimate the weight of a // PSBT input if it supplies all required information. func TestEstimateInputWeight(t *testing.T) { genScript := func(f func([]byte) ([]byte, error)) []byte { pkScript, _ := f([]byte{}) return pkScript } var ( witnessScaleFactor = blockchain.WitnessScaleFactor p2trScript, _ = txscript.PayToTaprootScript( &input.TaprootNUMSKey, ) dummyLeaf = txscript.TapLeaf{ LeafVersion: txscript.BaseLeafVersion, Script: []byte("some bitcoin script"), } dummyLeafHash = dummyLeaf.TapHash() ) testCases := []struct { name string in *psbt.PInput expectedErr error expectedErrString string // expectedWitnessWeight is the expected weight of the content // of the witness of the input (without the base input size that // is constant for all types of inputs). expectedWitnessWeight int }{{ name: "empty input", in: &psbt.PInput{}, expectedErr: ErrInputMissingUTXOInfo, }, { name: "empty pkScript", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{}, }, expectedErr: ErrUnsupportedScript, }, { name: "nested p2wpkh input", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{ PkScript: genScript(input.GenerateP2SH), }, }, expectedWitnessWeight: input.P2WKHWitnessSize + input.NestedP2WPKHSize*witnessScaleFactor, }, { name: "p2wpkh input", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{ PkScript: genScript(input.WitnessPubKeyHash), }, }, expectedWitnessWeight: input.P2WKHWitnessSize, }, { name: "p2wsh input", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{ PkScript: genScript(input.WitnessScriptHash), }, }, expectedErr: ErrScriptSpendFeeEstimationUnsupported, }, { name: "p2tr with no derivation info", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{ PkScript: p2trScript, }, }, expectedErrString: "cannot sign for taproot input " + "without taproot BIP0032 derivation info", }, { name: "p2tr key spend", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{ PkScript: p2trScript, }, SighashType: txscript.SigHashSingle, TaprootBip32Derivation: []*psbt.TaprootBip32Derivation{ {}, }, }, //nolint:lll expectedWitnessWeight: input.TaprootKeyPathCustomSighashWitnessSize, }, { name: "p2tr script spend", in: &psbt.PInput{ WitnessUtxo: &wire.TxOut{ PkScript: p2trScript, }, TaprootBip32Derivation: []*psbt.TaprootBip32Derivation{ { LeafHashes: [][]byte{ dummyLeafHash[:], }, }, }, TaprootLeafScript: []*psbt.TaprootTapLeafScript{ { LeafVersion: dummyLeaf.LeafVersion, Script: dummyLeaf.Script, }, }, }, expectedErr: ErrScriptSpendFeeEstimationUnsupported, }} // The non-witness weight for a TX with a single input. nonWitnessWeight := input.BaseTxSize + 1 + 1 + input.InputSize // The base weight of a witness TX. baseWeight := (nonWitnessWeight * witnessScaleFactor) + input.WitnessHeaderSize for _, tc := range testCases { tc := tc t.Run(tc.name, func(tt *testing.T) { estimator := input.TxWeightEstimator{} err := EstimateInputWeight(tc.in, &estimator) if tc.expectedErr != nil { require.Error(tt, err) require.ErrorIs(tt, err, tc.expectedErr) return } if tc.expectedErrString != "" { require.Error(tt, err) require.Contains( tt, err.Error(), tc.expectedErrString, ) return } require.NoError(tt, err) require.EqualValues( tt, baseWeight+tc.expectedWitnessWeight, estimator.Weight(), ) }) } } // TestBip32DerivationFromKeyDesc tests that we can correctly extract a BIP32 // derivation path from a key descriptor. func TestBip32DerivationFromKeyDesc(t *testing.T) { privKey, err := btcec.NewPrivateKey() require.NoError(t, err) testCases := []struct { name string keyDesc keychain.KeyDescriptor coinType uint32 expectedPath string expectedBip32Path []uint32 }{ { name: "testnet multi-sig family", keyDesc: keychain.KeyDescriptor{ PubKey: privKey.PubKey(), KeyLocator: keychain.KeyLocator{ Family: keychain.KeyFamilyMultiSig, Index: 123, }, }, coinType: chaincfg.TestNet3Params.HDCoinType, expectedPath: "m/1017'/1'/0'/0/123", expectedBip32Path: []uint32{ hardenedKey(keychain.BIP0043Purpose), hardenedKey(chaincfg.TestNet3Params.HDCoinType), hardenedKey(uint32(keychain.KeyFamilyMultiSig)), 0, 123, }, }, { name: "mainnet watchtower family", keyDesc: keychain.KeyDescriptor{ PubKey: privKey.PubKey(), KeyLocator: keychain.KeyLocator{ Family: keychain.KeyFamilyTowerSession, Index: 456, }, }, coinType: chaincfg.MainNetParams.HDCoinType, expectedPath: "m/1017'/0'/8'/0/456", expectedBip32Path: []uint32{ hardenedKey(keychain.BIP0043Purpose), hardenedKey(chaincfg.MainNetParams.HDCoinType), hardenedKey( uint32(keychain.KeyFamilyTowerSession), ), 0, 456, }, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(tt *testing.T) { d, trD, path := Bip32DerivationFromKeyDesc( tc.keyDesc, tc.coinType, ) require.NoError(tt, err) require.Equal(tt, tc.expectedPath, path) require.Equal(tt, tc.expectedBip32Path, d.Bip32Path) require.Equal(tt, tc.expectedBip32Path, trD.Bip32Path) serializedKey := tc.keyDesc.PubKey.SerializeCompressed() require.Equal(tt, serializedKey, d.PubKey) require.Equal(tt, serializedKey[1:], trD.XOnlyPubKey) }) } } // TestBip32DerivationFromAddress tests that we can correctly extract a BIP32 // derivation path from an address. func TestBip32DerivationFromAddress(t *testing.T) { testCases := []struct { name string addrType lnwallet.AddressType expectedAddr string expectedPath string expectedBip32Path []uint32 expectedPubKey string }{ { name: "p2wkh", addrType: lnwallet.WitnessPubKey, expectedAddr: firstAddress, expectedPath: "m/84'/0'/0'/0/0", expectedBip32Path: []uint32{ hardenedKey(waddrmgr.KeyScopeBIP0084.Purpose), hardenedKey(0), hardenedKey(0), 0, 0, }, expectedPubKey: firstAddressPubKey, }, { name: "p2tr", addrType: lnwallet.TaprootPubkey, expectedAddr: firstAddressTaproot, expectedPath: "m/86'/0'/0'/0/0", expectedBip32Path: []uint32{ hardenedKey(waddrmgr.KeyScopeBIP0086.Purpose), hardenedKey(0), hardenedKey(0), 0, 0, }, expectedPubKey: firstAddressTaprootPubKey, }, } w, _ := newTestWallet(t, netParams, seedBytes) for _, tc := range testCases { tc := tc addr, err := w.NewAddress( tc.addrType, false, lnwallet.DefaultAccountName, ) require.NoError(t, err) require.Equal(t, tc.expectedAddr, addr.String()) addrInfo, err := w.AddressInfo(addr) require.NoError(t, err) managedAddr, ok := addrInfo.(waddrmgr.ManagedPubKeyAddress) require.True(t, ok) d, trD, path, err := Bip32DerivationFromAddress(managedAddr) require.NoError(t, err) require.Equal(t, tc.expectedPath, path) require.Equal(t, tc.expectedBip32Path, d.Bip32Path) require.Equal(t, tc.expectedBip32Path, trD.Bip32Path) } }