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 Mar 16, 2022
1 parent 162244e commit 85b35fa
Show file tree
Hide file tree
Showing 9 changed files with 800 additions and 39 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -22,6 +22,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 Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -100,6 +100,8 @@ github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQ
github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU=
github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0=
github.com/lightningnetwork/lnd/tlv v1.0.2 h1:LG7H3Uw/mHYGnEeHRPg+STavAH+UsFvuBflD0PzcYFQ=
github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqWRMqEDCNxZJSKr9C4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand Down
133 changes: 110 additions & 23 deletions waddrmgr/address.go
Expand Up @@ -57,6 +57,10 @@ const (
// uses BIP-0086 (for the derivation path and for calculating the tap
// root hash/tweak).
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 @@ -142,6 +146,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 @@ -796,38 +811,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
42 changes: 40 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 @@ -147,6 +148,13 @@ func WitnessScriptHashIdentity(script []byte) Identity {
}
}

// TaprootIdentity returns the identity closure for a p2tr script.
func TaprootIdentity(taprootKey *btcec.PublicKey) Identity {
return func() []byte {
return schnorr.SerializePubKey(taprootKey)
}
}

// ScopeAddrSchema is the address schema of a particular KeyScope. This will be
// persisted within the database, and will be consulted when deriving any keys
// for a particular scope to know how to encode the public keys as addresses.
Expand Down Expand Up @@ -2110,6 +2118,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, TaprootIdentity(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 @@ -2179,7 +2217,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 @@ -2217,7 +2255,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 85b35fa

Please sign in to comment.