Skip to content

Commit

Permalink
mod+waddrmgr: add taproot script address
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Feb 22, 2022
1 parent 720b05d commit 6d1c60c
Show file tree
Hide file tree
Showing 9 changed files with 795 additions and 39 deletions.
3 changes: 3 additions & 0 deletions go.mod
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down
133 changes: 110 additions & 23 deletions waddrmgr/address.go
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
60 changes: 46 additions & 14 deletions waddrmgr/manager_test.go
Expand Up @@ -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"
Expand Down Expand Up @@ -892,6 +893,7 @@ func testImportScript(tc *testContext) bool {
name string
in []byte
isWitness bool
isTaproot bool
witnessVersion byte
isSecretScript bool
blockstamp BlockStamp
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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:
Expand Down
35 changes: 33 additions & 2 deletions waddrmgr/scoped_manager.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 6d1c60c

Please sign in to comment.