Skip to content

Commit

Permalink
Merge pull request btcsuite#792 from guggero/schnorr-taproot
Browse files Browse the repository at this point in the history
Taproot: Add taproot address and signing capabilities
  • Loading branch information
Roasbeef authored and buck54321 committed Apr 21, 2024
1 parent dce9d80 commit 35b9664
Show file tree
Hide file tree
Showing 10 changed files with 577 additions and 93 deletions.
159 changes: 135 additions & 24 deletions waddrmgr/address.go
Expand Up @@ -82,6 +82,16 @@ const (
TaprootScript
)

const (
// witnessVersionV0 is the SegWit v0 witness version used for p2wpkh and
// p2wsh outputs and addresses.
witnessVersionV0 byte = 0x00

// witnessVersionV1 is the SegWit v1 witness version used for p2tr
// outputs and addresses.
witnessVersionV1 byte = 0x01
)

// ManagedAddress is an interface that provides acces to information regarding
// an address managed by an address manager. Concrete implementations of this
// type may provide further fields to provide information specific to that type
Expand Down Expand Up @@ -173,6 +183,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 @@ -557,7 +578,8 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager,
case TaprootPubKey:
tapKey := txscript.ComputeTaprootKeyNoScript(pubKey)
address, err = ltcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey), m.rootManager.chainParams,
schnorr.SerializePubKey(tapKey),
m.rootManager.chainParams,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -700,6 +722,14 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager,
return managedAddr, nil
}

// clearTextScriptSetter is a non-exported interface to identify script types
// that allow their clear text script to be set.
type clearTextScriptSetter interface {
// setClearText sets the unencrypted script on the struct after
// unlocking/decrypting it.
setClearTextScript([]byte)
}

// baseScriptAddress represents the common fields of a pay-to-script-hash and
// a pay-to-witness-script-hash address.
type baseScriptAddress struct {
Expand All @@ -711,6 +741,8 @@ type baseScriptAddress struct {
scriptMutex sync.Mutex
}

var _ clearTextScriptSetter = (*baseScriptAddress)(nil)

// unlock decrypts and stores the associated script. It will fail if the key is
// invalid or the encrypted script is not available. The returned clear text
// script will always be a copy that may be safely used by the caller without
Expand Down Expand Up @@ -769,6 +801,13 @@ func (a *baseScriptAddress) Internal() bool {
return false
}

// setClearText sets the unencrypted script on the struct after unlocking/
// decrypting it.
func (a *baseScriptAddress) setClearTextScript(script []byte) {
a.scriptClearText = make([]byte, len(script))
copy(a.scriptClearText, script)
}

// scriptAddress represents a pay-to-script-hash address.
type scriptAddress struct {
baseScriptAddress
Expand Down Expand Up @@ -943,38 +982,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 ltcutil.Address
err error
)
isSecretScript bool) (ManagedScriptAddress, error) {

switch witnessVersion {
case 0x00:
address, err = ltcutil.NewAddressWitnessScriptHash(
scriptHash, m.rootManager.chainParams,
case witnessVersionV0:
address, err := ltcutil.NewAddressWitnessScriptHash(
scriptIdent, m.rootManager.chainParams,
)
if err != nil {
return nil, err
}

case 0x01:
address, err = ltcutil.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 := ltcutil.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 ltcutil.Address which represents the managed address.
// This will be a pay-to-taproot address.
//
// This is part of the ManagedAddress interface implementation.
func (a *taprootScriptAddress) Address() ltcutil.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)
}
17 changes: 16 additions & 1 deletion waddrmgr/db.go
Expand Up @@ -74,6 +74,7 @@ const (
adtImport addressType = 1 // not iota as they need to be stable for db
adtScript addressType = 2
adtWitnessScript addressType = 3
adtTaprootScript addressType = 4
)

// accountType represents a type of address stored in the database.
Expand Down Expand Up @@ -1622,6 +1623,11 @@ func fetchAddressByHash(ns walletdb.ReadBucket, scope *KeyScope,
return deserializeScriptAddress(row)
case adtWitnessScript:
return deserializeWitnessScriptAddress(row)
case adtTaprootScript:
// A taproot script address is just a normal script address that
// TLV encodes more stuff in the raw script part. But in the
// database we store the same fields.
return deserializeWitnessScriptAddress(row)
}

str := fmt.Sprintf("unsupported address type '%d'", row.addrType)
Expand Down Expand Up @@ -1849,8 +1855,17 @@ func putWitnessScriptAddress(ns walletdb.ReadWriteBucket, scope *KeyScope,
rawData := serializeWitnessScriptAddress(
witnessVersion, isSecretScript, encryptedHash, encryptedScript,
)

addrType := adtWitnessScript
if witnessVersion == witnessVersionV1 {
// A taproot script stores a TLV encoded blob of data in the
// raw data field. So we only really need to use a different
// storage type since all other fields stay the same.
addrType = adtTaprootScript
}

addrRow := dbAddressRow{
addrType: adtWitnessScript,
addrType: addrType,
account: account,
addTime: uint64(time.Now().Unix()),
syncStatus: status,
Expand Down

0 comments on commit 35b9664

Please sign in to comment.