diff --git a/go.mod b/go.mod index 3e5a423de3..35fd569a13 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf github.com/lightninglabs/neutrino v0.13.2 github.com/lightningnetwork/lnd/ticker v1.0.0 + github.com/lightningnetwork/lnd/tlv v1.0.2 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc @@ -36,6 +37,8 @@ replace github.com/btcsuite/btcd/btcec/v2 => github.com/Roasbeef/btcd/btcec/v2 v replace github.com/btcsuite/btcwallet/wallet/txauthor => ./wallet/txauthor +replace github.com/lightningnetwork/lnd/tlv => github.com/guggero/lnd/tlv v0.0.0-20220221153005-553bce541120 + // The old version of ginko that's used in btcd imports an ancient version of // gopkg.in/fsnotify.v1 that isn't go mod compatible. We fix that import error // by replacing ginko (which is only a test library anyway) with a more recent diff --git a/go.sum b/go.sum index ee1073fa8c..dadd528800 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/guggero/btcd v0.20.1-beta.0.20220222115559-e9cc6fa85ed2 h1:6x2LSpAbc0xrhcTiEKVuSp2U9wvL5PdC5Nr3L8wee2k= github.com/guggero/btcd v0.20.1-beta.0.20220222115559-e9cc6fa85ed2/go.mod h1:DV2Y1MUL8kKUq9uRpot9YvKjGuvDZ1OShTmzYPHIsMc= +github.com/guggero/lnd/tlv v0.0.0-20220221153005-553bce541120 h1:QwR6EGCvOBeHe0AigubF03eBwL/DazWgfzweEJd1JxQ= +github.com/guggero/lnd/tlv v0.0.0-20220221153005-553bce541120/go.mod h1:1MC5EfYK+3VS/JTVNyvCAy8BOQicaAorj5CfrTRVDYQ= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= diff --git a/waddrmgr/address.go b/waddrmgr/address.go index da5c96df79..b03b32e10b 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -55,6 +55,10 @@ const ( // TaprootPubKey represents a p2tr (pay-to-taproot) address type. TaprootPubKey + + // TaprootScript represents a p2tr (pay-to-taproot) address type that + // commits to a script and not just a single key. + TaprootScript ) const ( @@ -140,6 +144,17 @@ type ManagedScriptAddress interface { Script() ([]byte, error) } +// ManagedTaprootScriptAddress extends ManagedScriptAddress and represents a +// pay-to-taproot script address. It additionally provides information about the +// script. +type ManagedTaprootScriptAddress interface { + ManagedScriptAddress + + // TaprootScript returns all the information needed to derive the script + // tree root hash needed to arrive at the tweaked taproot key. + TaprootScript() (*Tapscript, error) +} + // managedAddress represents a public key address. It also may or may not have // the private key associated with the public key. type managedAddress struct { @@ -794,38 +809,110 @@ var _ ManagedScriptAddress = (*witnessScriptAddress)(nil) // newWitnessScriptAddress initializes and returns a new // pay-to-witness-script-hash address. -func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, +func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent, scriptEncrypted []byte, witnessVersion byte, - isSecretScript bool) (*witnessScriptAddress, error) { - - var ( - address btcutil.Address - err error - ) + isSecretScript bool) (ManagedScriptAddress, error) { switch witnessVersion { - case 0x00: - address, err = btcutil.NewAddressWitnessScriptHash( - scriptHash, m.rootManager.chainParams, + case witnessVersionV0: + address, err := btcutil.NewAddressWitnessScriptHash( + scriptIdent, m.rootManager.chainParams, ) + if err != nil { + return nil, err + } - case 0x01: - address, err = btcutil.NewAddressTaproot( - scriptHash, m.rootManager.chainParams, + return &witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, + }, nil + + case witnessVersionV1: + address, err := btcutil.NewAddressTaproot( + scriptIdent, m.rootManager.chainParams, ) + if err != nil { + return nil, err + } + + // Lift the x-only coordinate of the tweaked public key. + tweakedPubKey, err := schnorr.ParsePubKey(scriptIdent) + if err != nil { + return nil, fmt.Errorf("error lifting public key from "+ + "script ident: %v", err) + } + + return &taprootScriptAddress{ + witnessScriptAddress: witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, + }, + TweakedPubKey: tweakedPubKey, + }, nil + + default: + return nil, fmt.Errorf("unsupported witness version %d", + witnessVersion) } +} + +// taprootScriptAddress represents a pay-to-taproot address that commits to a +// script. +type taprootScriptAddress struct { + witnessScriptAddress + + TweakedPubKey *btcec.PublicKey +} + +// Enforce taprootScriptAddress satisfies the ManagedTaprootScriptAddress +// interface. +var _ ManagedTaprootScriptAddress = (*taprootScriptAddress)(nil) + +// AddrType returns the address type of the managed address. This can be used +// to quickly discern the address type without further processing +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) AddrType() AddressType { + return TaprootScript +} + +// Address returns the btcutil.Address which represents the managed address. +// This will be a pay-to-witness-script-hash address. +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) Address() btcutil.Address { + return a.address +} + +// AddrHash returns the script hash for the address. +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) AddrHash() []byte { + return schnorr.SerializePubKey(a.TweakedPubKey) +} + +// TaprootScript returns all the information needed to derive the script tree +// root hash needed to arrive at the tweaked taproot key. +func (a *taprootScriptAddress) TaprootScript() (*Tapscript, error) { + // Need to decrypt our internal script first. We need to be unlocked for + // this. + script, err := a.Script() if err != nil { return nil, err } - return &witnessScriptAddress{ - baseScriptAddress: baseScriptAddress{ - manager: m, - account: account, - scriptEncrypted: scriptEncrypted, - }, - address: address, - witnessVersion: witnessVersion, - isSecretScript: isSecretScript, - }, nil + // Decode the additional TLV encoded data. + return tlvDecodeTaprootTaprootScript(script) } diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index 03ada7d56e..ef04742c4c 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" @@ -892,6 +893,7 @@ func testImportScript(tc *testContext) bool { name string in []byte isWitness bool + isTaproot bool witnessVersion byte isSecretScript bool blockstamp BlockStamp @@ -973,18 +975,23 @@ func testImportScript(tc *testContext) bool { }, }, { - name: "p2tr multisig", - isWitness: true, + name: "p2tr tapscript with all tap leaves", + isTaproot: true, witnessVersion: 1, isSecretScript: true, - in: hexToBytes("52210305a662958b547fe25a71cd28fc7ef1c2" + - "ad4a79b12f34fc40137824b88e61199d21038552c09d9" + - "a709c8cbba6e472307d3f8383f46181895a76e01e258f" + - "09033b4a78210205ad9a838cff17d79fee2841bec72e9" + - "9b6fd4e62fd9214fcf845b1cf8438062053ae"), + // The encoded *Tapscript struct for a script with all + // tap script leaves known. + in: hexToBytes( + "0101000221c00ef94ee79c07cbd1988fffd6e6aea1e2" + + "5c3b033a2fd64fe14a9b955e5355f0c60346" + + "1d0101c0021876a914f6c97547d73156abb3" + + "00ae059905c4acaadd09dd88270101c00222" + + "200ef94ee79c07cbd1988fffd6e6aea1e25c" + + "3b033a2fd64fe14a9b955e5355f0c6ac", + ), expected: expectedAddr{ - address: "bc1pc57jdm7kcnufnc339fvy2caflj6lkfeqasdfghftl7dd77dfpresqu7vep", - addressHash: hexToBytes("c53d26efd6c4f899e2312a584563a9fcb5fb2720ec1a945d2bff9adf79a908f3"), + address: "bc1pu92qt24cl4spyp4rsj9sa3y4ma6a3fszgewcmway9f6f80vgnduq5lnd0u", + addressHash: hexToBytes("e15405aab8fd601206a3848b0ec495df75d8a602465d8dbba42a7493bd889b78"), internal: false, imported: true, compressed: true, @@ -1023,13 +1030,27 @@ func testImportScript(tc *testContext) bool { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error - if test.isWitness { + switch { + case test.isWitness: addr, err = tc.manager.ImportWitnessScript( ns, test.in, &test.blockstamp, test.witnessVersion, test.isSecretScript, ) - } else { + + case test.isTaproot: + var script *Tapscript + script, err = tlvDecodeTaprootTaprootScript( + test.in, + ) + require.NoError(tc.t, err) + addr, err = tc.manager.ImportTaprootScript( + ns, script, &test.blockstamp, + test.witnessVersion, + test.isSecretScript, + ) + + default: addr, err = tc.manager.ImportScript( ns, test.in, &test.blockstamp, ) @@ -1068,10 +1089,21 @@ func testImportScript(tc *testContext) bool { scriptHash[:], chainParams, ) - case test.isWitness && test.witnessVersion == 1: - scriptHash := sha256.Sum256(test.in) + case test.isTaproot: + var ( + script *Tapscript + taprootKey *btcec.PublicKey + ) + script, err = tlvDecodeTaprootTaprootScript( + test.in, + ) + require.NoError(tc.t, err) + taprootKey, err = script.TaprootKey() + require.NoError(tc.t, err) + utilAddr, err = btcutil.NewAddressTaproot( - scriptHash[:], chainParams, + schnorr.SerializePubKey(taprootKey), + chainParams, ) default: diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 45bbd66e43..f28edca210 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" @@ -2093,6 +2094,36 @@ func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket, ) } +// ImportTaprootScript imports a user-provided taproot script into the address +// manager. The imported script will act as a pay-to-taproot address. +func (s *ScopedKeyManager) ImportTaprootScript(ns walletdb.ReadWriteBucket, + tapscript *Tapscript, bs *BlockStamp, witnessVersion byte, + isSecretScript bool) (ManagedTaprootScriptAddress, error) { + + // Make sure we have everything we need to calculate the script root and + // tweak the taproot key. + taprootKey, err := tapscript.TaprootKey() + if err != nil { + return nil, fmt.Errorf("error calculating script root: %v", err) + } + + script, err := tlvEncodeTaprootScript(tapscript) + if err != nil { + return nil, fmt.Errorf("error encoding taproot script: %v", err) + } + + managedAddr, err := s.importScriptAddress( + ns, schnorr.SerializePubKey(taprootKey), script, bs, + TaprootScript, witnessVersion, isSecretScript, + ) + if err != nil { + return nil, err + } + + // We know this is a taproot address at this point. + return managedAddr.(ManagedTaprootScriptAddress), nil +} + // importScriptAddress imports a new pay-to-script or pay-to-witness-script // address. func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, @@ -2161,7 +2192,7 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // Save the new imported address to the db and update start block (if // needed) in a single transaction. switch addrType { - case WitnessScript: + case WitnessScript, TaprootScript: err = putWitnessScriptAddress( ns, &s.scope, scriptIdent, ImportedAddrAccount, ssNone, witnessVersion, isSecretScript, encryptedHash, @@ -2199,7 +2230,7 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // should not be cleared out from under the caller. var managedAddr ManagedScriptAddress switch addrType { - case WitnessScript: + case WitnessScript, TaprootScript: managedAddr, err = newWitnessScriptAddress( s, ImportedAddrAccount, scriptIdent, encryptedScript, witnessVersion, isSecretScript, diff --git a/waddrmgr/tapscript.go b/waddrmgr/tapscript.go new file mode 100644 index 0000000000..67aab41900 --- /dev/null +++ b/waddrmgr/tapscript.go @@ -0,0 +1,80 @@ +package waddrmgr + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/txscript" +) + +// TapscriptType is a special type denoting the different variants of +// tapscripts. +type TapscriptType uint8 + +const ( + // TapscriptTypeFullTree is the type of tapscript that knows its full + // tree with all individual leaves present. + TapscriptTypeFullTree TapscriptType = 0 + + // TapscriptTypePartialReveal is the type of tapscript that only knows + // a single revealed leaf and the merkle/inclusion proof for the rest of + // the tree. + TapscriptTypePartialReveal TapscriptType = 1 +) + +// Tapscript is a struct that holds either a full taproot tapscript with all +// individual leaves or a single leaf and the corresponding proof to arrive at +// the root hash. +type Tapscript struct { + // Type is the type of the tapscript. + Type TapscriptType + + // ControlBlock houses the main information about the internal key and + // the resulting key's parity. And, in case of the + // TapscriptTypePartialReveal type, the control block also contains the + // inclusion proof and the leaf version for the revealed script. + ControlBlock *txscript.ControlBlock + + // Leaves is the full set of tap leaves in their proper order. This is + // only set if the Type is TapscriptTypeFullTree. + Leaves []txscript.TapLeaf + + // RevealedScript is the script of the single revealed script. Is only + // set if the Type is TapscriptTypePartialReveal. + RevealedScript []byte +} + +// TaprootKey calculates the tweaked taproot key from the given internal key and +// the tree information in this tapscript struct. If any information required to +// calculate the root hash is missing, this method returns an error. +func (t *Tapscript) TaprootKey() (*btcec.PublicKey, error) { + if t.ControlBlock == nil || t.ControlBlock.InternalKey == nil { + return nil, fmt.Errorf("internal key is missing") + } + + switch t.Type { + case TapscriptTypeFullTree: + if len(t.Leaves) == 0 { + return nil, fmt.Errorf("missing leaves") + } + + tree := txscript.AssembleTaprootScriptTree(t.Leaves...) + rootHash := tree.RootNode.TapHash() + return txscript.ComputeTaprootOutputKey( + t.ControlBlock.InternalKey, rootHash[:], + ), nil + + case TapscriptTypePartialReveal: + if len(t.RevealedScript) == 0 { + return nil, fmt.Errorf("revealed script missing") + } + + rootHash := t.ControlBlock.RootHash(t.RevealedScript) + return txscript.ComputeTaprootOutputKey( + t.ControlBlock.InternalKey, rootHash, + ), nil + + default: + return nil, fmt.Errorf("unknown tapscript type %d", t.Type) + } +} diff --git a/waddrmgr/tapscript_test.go b/waddrmgr/tapscript_test.go new file mode 100644 index 0000000000..27c9c2ea13 --- /dev/null +++ b/waddrmgr/tapscript_test.go @@ -0,0 +1,88 @@ +package waddrmgr + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + "github.com/stretchr/testify/require" +) + +var ( + testInternalKey, _ = btcec.ParsePubKey(hexToBytes( + "020ef94ee79c07cbd1988fffd6e6aea1e25c3b033a2fd64fe14a9b955e53" + + "55f0c6", + )) + + testScript1 = hexToBytes( + "76a914f6c97547d73156abb300ae059905c4acaadd09dd88", + ) + testScript2 = hexToBytes( + "200ef94ee79c07cbd1988fffd6e6aea1e25c3b033a2fd64fe14a9b955e53" + + "55f0c6ac", + ) + testScript1Proof = hexToBytes( + "6c2e4bb01e316abaaee288d69c06cc608cedefd6e1a06813786c4ec51b6e" + + "1d38", + ) + + testTaprootKey = hexToBytes( + "e15405aab8fd601206a3848b0ec495df75d8a602465d8dbba42a7493bd88" + + "9b78", + ) +) + +// TestTaprootKey tests that the taproot tweaked key can be calculated correctly +// for both a tree with all leaves known as well as a partially revealed tree +// with an inclusion/merkle proof. +func TestTaprootKey(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + given *Tapscript + expected []byte + }{{ + name: "full tree", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + Leaves: []txscript.TapLeaf{ + txscript.NewBaseTapLeaf(testScript1), + txscript.NewBaseTapLeaf(testScript2), + }, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testInternalKey, + LeafVersion: txscript.BaseLeafVersion, + }, + }, + expected: testTaprootKey, + }, { + name: "partial tree with proof", + given: &Tapscript{ + Type: TapscriptTypePartialReveal, + RevealedScript: testScript2, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testInternalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: testScript1Proof, + }, + }, + expected: testTaprootKey, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(tt *testing.T) { + taprootKey, err := tc.given.TaprootKey() + require.NoError(tt, err) + + require.Equal( + tt, tc.expected, schnorr.SerializePubKey( + taprootKey, + ), + ) + }) + } +} diff --git a/waddrmgr/tlv.go b/waddrmgr/tlv.go new file mode 100644 index 0000000000..bf5cfd228d --- /dev/null +++ b/waddrmgr/tlv.go @@ -0,0 +1,271 @@ +package waddrmgr + +import ( + "bytes" + "fmt" + "io" + + "github.com/btcsuite/btcd/txscript" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + typeTapscriptType tlv.Type = 1 + typeTapscriptControlBlock tlv.Type = 2 + typeTapscriptLeaves tlv.Type = 3 + typeTapscriptRevealedScript tlv.Type = 4 + + typeTapLeafVersion tlv.Type = 1 + typeTapLeafScript tlv.Type = 2 +) + +// tlvEncodeTaprootScript encodes the given internal key and full set of taproot +// script leaves into a byte slice encoded as a TLV stream. +func tlvEncodeTaprootScript(s *Tapscript) ([]byte, error) { + if s == nil { + return nil, fmt.Errorf("cannot encode nil script") + } + + typ := uint8(s.Type) + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(typeTapscriptType, &typ), + } + + if s.ControlBlock != nil { + if s.ControlBlock.InternalKey == nil { + return nil, fmt.Errorf("control block is missing " + + "internal key") + } + + blockBytes, err := s.ControlBlock.ToBytes() + if err != nil { + return nil, fmt.Errorf("error encoding control block: "+ + "%v", err) + } + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + typeTapscriptControlBlock, &blockBytes, + )) + } + + if len(s.Leaves) > 0 { + tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord( + typeTapscriptLeaves, &s.Leaves, func() uint64 { + return recordSize(leavesEncoder, &s.Leaves) + }, leavesEncoder, leavesDecoder, + )) + } + + if len(s.RevealedScript) > 0 { + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + typeTapscriptRevealedScript, &s.RevealedScript, + )) + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + err = tlvStream.Encode(&buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// tlvDecodeTaprootTaprootScript decodes the given byte slice as a TLV stream +// and attempts to parse the taproot internal key and full set of leaves from +// it. +func tlvDecodeTaprootTaprootScript(tlvData []byte) (*Tapscript, error) { + + var ( + typ uint8 + controlBlockBytes []byte + s = &Tapscript{} + ) + + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeTapscriptType, &typ), + tlv.MakePrimitiveRecord( + typeTapscriptControlBlock, &controlBlockBytes, + ), + tlv.MakeDynamicRecord( + typeTapscriptLeaves, &s.Leaves, func() uint64 { + return recordSize(leavesEncoder, &s.Leaves) + }, leavesEncoder, leavesDecoder, + ), + tlv.MakePrimitiveRecord( + typeTapscriptRevealedScript, &s.RevealedScript, + ), + ) + if err != nil { + return nil, err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes(bytes.NewReader( + tlvData, + )) + if err != nil { + return nil, err + } + + s.Type = TapscriptType(typ) + if t, ok := parsedTypes[typeTapscriptControlBlock]; ok && t == nil { + s.ControlBlock, err = txscript.ParseControlBlock( + controlBlockBytes, + ) + if err != nil { + return nil, fmt.Errorf("error decoding control block: "+ + "%v", err) + } + } + + return s, nil +} + +// leavesEncoder is a custom TLV decoder for a slice of tap leaf records. +func leavesEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*[]txscript.TapLeaf); ok { + for _, c := range *v { + leafVersion := uint8(c.LeafVersion) + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord( + typeTapLeafVersion, &leafVersion, + ), + } + + if len(c.Script) > 0 { + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + typeTapLeafScript, &c.Script, + ), + ) + + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + var leafTLVBytes bytes.Buffer + err = tlvStream.Encode(&leafTLVBytes) + if err != nil { + return err + } + + // We encode the record with a varint length followed by + // the _raw_ TLV bytes. + tlvLen := uint64(len(leafTLVBytes.Bytes())) + if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil { + return err + } + + _, err = w.Write(leafTLVBytes.Bytes()) + if err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "[]txscript.TapLeaf") +} + +// leavesDecoder is a custom TLV decoder for a slice of tap leaf records. +func leavesDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*[]txscript.TapLeaf); ok { + var leaves []txscript.TapLeaf + + // Using the length information given, we'll create a new + // limited reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + for { + // Read out the varint that encodes the size of this + // inner TLV record. + blobSize, err := tlv.ReadVarInt(r, buf) + if err == io.EOF { + break + } else if err != nil { + return err + } + + innerInnerTlvReader := io.LimitedReader{ + R: &innerTlvReader, + N: int64(blobSize), + } + + var ( + leafVersion uint8 + script []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typeTapLeafVersion, &leafVersion, + ), + tlv.MakePrimitiveRecord( + typeTapLeafScript, &script, + ), + ) + if err != nil { + return err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes( + &innerInnerTlvReader, + ) + if err != nil { + return err + } + + leaf := txscript.TapLeaf{ + LeafVersion: txscript.TapscriptLeafVersion( + leafVersion, + ), + } + + // Only set script when actually parsed to make + // difference between nil and empty slice work + // correctly. The parsedTypes entry must be nil if it + // was parsed fully. + if t, ok := parsedTypes[typeTapLeafScript]; ok && t == nil { + leaf.Script = script + } + + leaves = append(leaves, leaf) + } + + *v = leaves + return nil + } + + return tlv.NewTypeForDecodingErr(val, "[]txscript.TapLeaf", l, l) +} + +// recordSize returns the amount of bytes this TLV record will occupy when +// encoded. +func recordSize(encoder tlv.Encoder, v interface{}) uint64 { + var ( + b bytes.Buffer + buf [8]byte + ) + + // We know that encoding works since the tests pass in the build this + // file is checked into, so we'll simplify things and simply encode it + // ourselves then report the total amount of bytes used. + if err := encoder(&b, v, &buf); err != nil { + // This should never error out, but we log it just in case it + // does. + log.Errorf("encoding the record failed: %v", err) + } + + return uint64(len(b.Bytes())) +} diff --git a/waddrmgr/tlv_test.go b/waddrmgr/tlv_test.go new file mode 100644 index 0000000000..a3f04ef2b9 --- /dev/null +++ b/waddrmgr/tlv_test.go @@ -0,0 +1,162 @@ +package waddrmgr + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + "github.com/stretchr/testify/require" +) + +var ( + testPubKey, _ = schnorr.ParsePubKey(hexToBytes( + "29faddf1254d490d6add49e2b08cf52b561038c72baec0edb3cfacff71" + + "ff1021", + )) + testScript = []byte{99, 88, 77, 66, 55, 44} + testProof = [32]byte{99, 88, 77, 66} +) + +// TestTlvEncodeDecode tests encoding and decoding of taproot script TLV data. +func TestTlvEncodeDecode(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + given *Tapscript + expected *Tapscript + expectedErrEncode string + expectedErrDecode string + }{{ + name: "nil", + expectedErrEncode: "cannot encode nil script", + }, { + name: "empty", + given: &Tapscript{}, + expected: &Tapscript{}, + }, { + name: "no leaves", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + }, + }, + expected: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: []byte{}, + }, + }, + }, { + name: "no pubkey", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{}, + }, + expectedErrEncode: "control block is missing internal key", + }, { + name: "empty leaf", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + }, + Leaves: []txscript.TapLeaf{{}}, + }, + expected: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: []byte{}, + }, + Leaves: []txscript.TapLeaf{{}}, + }, + }, { + name: "full key and leaves", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + }, + Leaves: []txscript.TapLeaf{ + txscript.NewBaseTapLeaf(testScript), + }, + }, + expected: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: []byte{}, + }, + Leaves: []txscript.TapLeaf{ + txscript.NewBaseTapLeaf(testScript), + }, + }, + }, { + name: "invalid proof", + given: &Tapscript{ + Type: TapscriptTypePartialReveal, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: testScript, + }, + RevealedScript: testScript, + }, + expectedErrDecode: "error decoding control block: invalid max " + + "block size", + }, { + name: "inclusion proof no leaves", + given: &Tapscript{ + Type: TapscriptTypePartialReveal, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: testProof[:], + }, + RevealedScript: testScript, + }, + expected: &Tapscript{ + Type: TapscriptTypePartialReveal, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: testProof[:], + }, + RevealedScript: testScript, + }, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(tt *testing.T) { + data, err := tlvEncodeTaprootScript(tc.given) + + if tc.expectedErrEncode != "" { + require.Error(tt, err) + require.Contains( + tt, err.Error(), tc.expectedErrEncode, + ) + + return + } + + require.NoError(tt, err) + require.NotEmpty(tt, data) + + decoded, err := tlvDecodeTaprootTaprootScript(data) + if tc.expectedErrDecode != "" { + require.Error(tt, err) + require.Contains( + tt, err.Error(), tc.expectedErrDecode, + ) + + return + } + + require.NoError(tt, err) + + require.Equal(tt, tc.expected, decoded) + }) + } +}