stdaddr: Add v0 p2pkh-ecdsa-secp256k1 support.

This adds a new type to fully support version 0
pay-to-pubkey-hash-ecdsa-secp256k1 addresses along with associated
tests.

This is part of a series of commits to fully implement the stdaddr
package.
This commit is contained in:
Dave Collins 2021-03-05 08:34:15 -06:00
parent 461a15a626
commit c6d7cd71ac
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
6 changed files with 392 additions and 1 deletions

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/decred/dcrd/chaincfg/v3 v3.0.0
github.com/decred/dcrd/connmgr/v3 v3.0.0
github.com/decred/dcrd/container/apbf v1.0.0
github.com/decred/dcrd/crypto/blake256 v1.0.0
github.com/decred/dcrd/crypto/ripemd160 v1.0.1
github.com/decred/dcrd/database/v2 v2.0.3-0.20210129190127-4ebd135a82f1
github.com/decred/dcrd/dcrec v1.0.0

View File

@ -283,9 +283,16 @@ func NewAddressPubKeySchnorrSecp256k1(scriptVersion uint16,
// compressed format since it occupies less space on the chain and is more
// consistent with other address formats where uncompressed public keys are NOT
// supported.
//
// NOTE: Version 0 scripts are the only currently supported version.
func NewAddressPubKeyHashEcdsaSecp256k1(scriptVersion uint16, pkHash []byte,
params AddressParams) (Address, error) {
switch scriptVersion {
case 0:
return NewAddressPubKeyHashEcdsaSecp256k1V0(pkHash, params)
}
str := fmt.Sprintf("pubkey hash addresses for version %d are not "+
"supported", scriptVersion)
return nil, makeError(ErrUnsupportedScriptVersion, str)

View File

@ -651,6 +651,108 @@ func TestAddresses(t *testing.T) {
decodeErr: nil,
version: 0,
payScript: "21030844ee70d8384d5250e9bb3a6a73d4b5bec770e8b31d6a0ae9fb739009d91af552be",
}, {
// ---------------------------------------------------------------------
// Negative P2PKH ECDSA secp256k1 tests.
// ---------------------------------------------------------------------
name: "p2pkh-ecdsa-secp256k1 wrong hash length",
makeAddr: func() (Address, error) {
hash := hexToBytes("000ef030107fd26e0b6bf40512bca2ceb1dd80adaa")
return NewAddressPubKeyHashEcdsaSecp256k1(0, hash, mainNetParams)
},
makeErr: ErrInvalidHashLen,
}, {
name: "p2pkh-ecdsa-secp256k1 unsupported script version",
makeAddr: func() (Address, error) {
hash := hexToBytes("2789d58cfa0957d206f025c2af056fc8a77cebb0")
return NewAddressPubKeyHashEcdsaSecp256k1(9999, hash, mainNetParams)
},
makeErr: ErrUnsupportedScriptVersion,
}, {
// ---------------------------------------------------------------------
// Positive P2PKH ECDSA secp256k1 tests.
// ---------------------------------------------------------------------
name: "mainnet p2pkh-ecdsa-secp256k1",
makeAddr: func() (Address, error) {
hash := hexToBytes("2789d58cfa0957d206f025c2af056fc8a77cebb0")
return NewAddressPubKeyHashEcdsaSecp256k1(0, hash, mainNetParams)
},
makeErr: nil,
addr: "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu",
net: mainNetParams,
decodeErr: nil,
version: 0,
payScript: "76a9142789d58cfa0957d206f025c2af056fc8a77cebb088ac",
voteScript: "ba76a9142789d58cfa0957d206f025c2af056fc8a77cebb088ac",
rewardAmount: 1e8,
feeLimits: 0x5800,
rewardScript: "6a1e2789d58cfa0957d206f025c2af056fc8a77cebb000e1f505000000000058",
changeScript: "bd76a9142789d58cfa0957d206f025c2af056fc8a77cebb088ac",
commitScript: "bb76a9142789d58cfa0957d206f025c2af056fc8a77cebb088ac",
revokeScript: "bc76a9142789d58cfa0957d206f025c2af056fc8a77cebb088ac",
trsyScript: "c376a9142789d58cfa0957d206f025c2af056fc8a77cebb088ac",
}, {
name: "mainnet p2pkh-ecdsa-secp256k1 2",
makeAddr: func() (Address, error) {
hash := hexToBytes("229ebac30efd6a69eec9c1a48e048b7c975c25f2")
return NewAddressPubKeyHashEcdsaSecp256k1(0, hash, mainNetParams)
},
makeErr: nil,
addr: "DsU7xcg53nxaKLLcAUSKyRndjG78Z2VZnX9",
net: mainNetParams,
decodeErr: nil,
version: 0,
payScript: "76a914229ebac30efd6a69eec9c1a48e048b7c975c25f288ac",
voteScript: "ba76a914229ebac30efd6a69eec9c1a48e048b7c975c25f288ac",
rewardAmount: 9556193632,
feeLimits: 0x5900,
rewardScript: "6a1e229ebac30efd6a69eec9c1a48e048b7c975c25f260f19739020000000059",
changeScript: "bd76a914229ebac30efd6a69eec9c1a48e048b7c975c25f288ac",
commitScript: "bb76a914229ebac30efd6a69eec9c1a48e048b7c975c25f288ac",
revokeScript: "bc76a914229ebac30efd6a69eec9c1a48e048b7c975c25f288ac",
trsyScript: "c376a914229ebac30efd6a69eec9c1a48e048b7c975c25f288ac",
}, {
name: "testnet p2pkh-ecdsa-secp256k1",
makeAddr: func() (Address, error) {
hash := hexToBytes("f15da1cb8d1bcb162c6ab446c95757a6e791c916")
return NewAddressPubKeyHashEcdsaSecp256k1(0, hash, testNetParams)
},
makeErr: nil,
addr: "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h",
net: testNetParams,
decodeErr: nil,
version: 0,
payScript: "76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
voteScript: "ba76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
rewardAmount: 2428220961,
feeLimits: 0x5800,
rewardScript: "6a1ef15da1cb8d1bcb162c6ab446c95757a6e791c91621b6bb90000000000058",
changeScript: "bd76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
commitScript: "bb76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
revokeScript: "bc76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
trsyScript: "c376a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
}, {
name: "regnet p2pkh-sep256k1-ecdsa",
makeAddr: func() (Address, error) {
hash := hexToBytes("f15da1cb8d1bcb162c6ab446c95757a6e791c916")
return NewAddressPubKeyHashEcdsaSecp256k1(0, hash, regNetParams)
},
makeErr: nil,
addr: "RsWM2w5LPJip56uxcZ1Scq7Tcbg97EfiwPA",
net: regNetParams,
decodeErr: nil,
version: 0,
payScript: "76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
voteScript: "ba76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
rewardAmount: 2428220961,
feeLimits: 0x5800,
rewardScript: "6a1ef15da1cb8d1bcb162c6ab446c95757a6e791c91621b6bb90000000000058",
changeScript: "bd76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
commitScript: "bb76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
revokeScript: "bc76a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
trsyScript: "c376a914f15da1cb8d1bcb162c6ab446c95757a6e791c91688ac",
}}
for _, test := range tests {
@ -840,6 +942,24 @@ func TestAddresses(t *testing.T) {
continue
}
// Ensure the AddressPubKeyHash method for the address types that
// support it returns the expected address.
var pkhAddr Address
var wantPkhAddr string
switch a := decodedAddr.(type) {
case *AddressPubKeyEcdsaSecp256k1V0:
id := test.net.AddrIDPubKeyHashECDSAV0()
wantPkhAddr = base58.CheckEncode(Hash160(a.serializedPubKey), id)
pkhAddr = a.AddressPubKeyHash()
}
if pkhAddr != nil {
gotAddr := pkhAddr.Address()
if gotAddr != wantPkhAddr {
t.Errorf("%s: mismatched pkh address -- got %s, want %s",
test.name, gotAddr, wantPkhAddr)
}
}
// Ensure the Hash160 method for the addresses that support it returns
// the expected value.
if h160er, ok := decodedAddr.(Hash160er); ok {

View File

@ -5,10 +5,13 @@
package stdaddr
import (
"encoding/binary"
"errors"
"fmt"
"github.com/decred/base58"
"github.com/decred/dcrd/crypto/blake256"
"github.com/decred/dcrd/crypto/ripemd160"
"github.com/decred/dcrd/dcrec"
"github.com/decred/dcrd/dcrec/edwards/v2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
@ -20,12 +23,23 @@ import (
// create a cyclic dependency since txscript will need to depend on this package
// for signing.
const (
opData20 = 0x14
opData30 = 0x1e
opData32 = 0x20
opData33 = 0x21
op1 = 0x51
op2 = 0x52
opReturn = 0x6a
opDup = 0x76
opEqualVerify = 0x88
opHash160 = 0xa9
opCheckSig = 0xac
opSSTx = 0xba
opSSGen = 0xbb
opSSRTx = 0xbc
opSSTxChange = 0xbd
opCheckSigAlt = 0xbe
opTGen = 0xc3
)
const (
@ -41,6 +55,16 @@ const (
// pubkey address signature type byte for those that deal with compressed
// secp256k1 pubkeys to specify the omitted y coordinate is odd.
sigTypeSecp256k1PubKeyCompOddFlag = uint8(1 << 7)
// commitP2SHFlag specifies the bitmask to apply to an amount in a ticket
// commitment in order to specify if it is a pay-to-script-hash commitment.
// The value is derived from the fact it is encoded as the most significant
// bit in the amount.
commitP2SHFlag = uint64(1 << 63)
// p2pkhPaymentScriptLen is the length of a standard version 0 P2PKH script
// for secp256k1+ecdsa.
p2pkhPaymentScriptLen = 25
)
// AddressParamsV0 defines an interface that is used to provide the parameters
@ -83,6 +107,14 @@ func encodeAddressV0(data []byte, netID [2]byte) string {
return base58.CheckEncode(data, netID)
}
// Hash160 calculates the hash ripemd160(blake256(b)).
func Hash160(buf []byte) []byte {
b256Hash := blake256.Sum256(buf)
hasher := ripemd160.New()
hasher.Write(b256Hash[:])
return hasher.Sum(nil)
}
// AddressPubKeyEcdsaSecp256k1V0 specifies an address that represents a payment
// destination which imposes an encumbrance that requires a valid ECDSA
// signature for a specific secp256k1 public key.
@ -97,8 +129,10 @@ type AddressPubKeyEcdsaSecp256k1V0 struct {
serializedPubKey []byte
}
// Ensure AddressPubKeyEcdsaSecp256k1V0 implements the Address interface.
// Ensure AddressPubKeyEcdsaSecp256k1V0 implements the Address and
// AddressPubKeyHasher interfaces.
var _ Address = (*AddressPubKeyEcdsaSecp256k1V0)(nil)
var _ AddressPubKeyHasher = (*AddressPubKeyEcdsaSecp256k1V0)(nil)
// NewAddressPubKeyEcdsaSecp256k1V0Raw returns an address that represents a
// payment destination which imposes an encumbrance that requires a valid ECDSA
@ -203,6 +237,23 @@ func (addr *AddressPubKeyEcdsaSecp256k1V0) PaymentScript() (uint16, []byte) {
return 0, script[:]
}
// AddressPubKeyHash returns the address converted to a
// pay-to-pubkey-hash-ecdsa-secp256k1 address.
//
// Note that the hash used in resulting address is the hash of the serialized
// public key and the address constructor intentionally only supports public
// keys in the compressed format. In other words, the resulting address will
// impose an encumbrance that requires the public key to be provided in the
// compressed format.
func (addr *AddressPubKeyEcdsaSecp256k1V0) AddressPubKeyHash() Address {
pkHash := Hash160(addr.serializedPubKey)
addrPKH := &AddressPubKeyHashEcdsaSecp256k1V0{
netID: addr.pubKeyHashID,
}
copy(addrPKH.hash[:], pkHash)
return addrPKH
}
// String returns a human-readable string for the address.
//
// This is equivalent to calling Address, but is provided so the type can be
@ -439,6 +490,213 @@ func (addr *AddressPubKeySchnorrSecp256k1V0) String() string {
return addr.Address()
}
// AddressPubKeyHashEcdsaSecp256k1V0 specifies an address that represents a
// payment destination which imposes an encumbrance that requires a secp256k1
// public key that hashes to the given public key hash along with a valid ECDSA
// signature for that public key.
//
// This is commonly referred to as pay-to-pubkey-hash (P2PKH) for legacy
// reasons, however, since it is possible to support multiple algorithm and
// signature scheme combinations, it is technically more accurate to refer to it
// as pay-to-pubkey-hash-ecdsa-secp256k1.
type AddressPubKeyHashEcdsaSecp256k1V0 struct {
netID [2]byte
hash [ripemd160.Size]byte
}
// Ensure AddressPubKeyHashEcdsaSecp256k1V0 implements the Address,
// StakeAddress, and Hash160er interfaces.
var _ Address = (*AddressPubKeyHashEcdsaSecp256k1V0)(nil)
var _ StakeAddress = (*AddressPubKeyHashEcdsaSecp256k1V0)(nil)
var _ Hash160er = (*AddressPubKeyHashEcdsaSecp256k1V0)(nil)
// NewAddressPubKeyHashEcdsaSecp256k1V0 returns an address that represents a
// payment destination which imposes an encumbrance that requires a secp256k1
// public key that hashes to the provided public key hash along with a valid
// ECDSA signature for that public key using version 0 scripts.
//
// The provided public key hash must be 20 bytes and is expected to be the
// Hash160 of the associated secp256k1 public key serialized in the _compressed_
// format.
//
// It is important to note that while it is technically possible for legacy
// reasons to create this specific type of address based on the hash of a public
// key in the uncompressed format, so long as it is also redeemed with that same
// public key in uncompressed format, it is *HIGHLY* recommended to use the
// compressed format since it occupies less space on the chain and is more
// consistent with other address formats where uncompressed public keys are NOT
// supported.
func NewAddressPubKeyHashEcdsaSecp256k1V0(pkHash []byte,
params AddressParamsV0) (*AddressPubKeyHashEcdsaSecp256k1V0, error) {
// Check for a valid script hash length.
if len(pkHash) != ripemd160.Size {
str := fmt.Sprintf("public key hash is %d bytes vs required %d bytes",
len(pkHash), ripemd160.Size)
return nil, makeError(ErrInvalidHashLen, str)
}
addr := &AddressPubKeyHashEcdsaSecp256k1V0{
netID: params.AddrIDPubKeyHashECDSAV0(),
}
copy(addr.hash[:], pkHash)
return addr, nil
}
// Address returns the string encoding of the payment address for the associated
// script version and payment script.
//
// This is part of the Address interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) Address() string {
// The format for the data portion of addresses that encode 160-bit hashes
// is merely the hash itself:
// 20-byte ripemd160 hash
return encodeAddressV0(addr.hash[:ripemd160.Size], addr.netID)
}
// putPaymentScript serializes the payment script associated with the address
// directly into the passed byte slice which must be at least
// p2pkhPaymentScriptLen bytes in length or it will panic.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) putPaymentScript(script []byte) {
// A pay-to-pubkey-hash-ecdsa-secp256k1 script is of the form:
// DUP HASH160 <20-byte hash> EQUALVERIFY CHECKSIG
script[0] = opDup
script[1] = opHash160
script[2] = opData20
copy(script[3:23], addr.hash[:])
script[23] = opEqualVerify
script[24] = opCheckSig
}
// PaymentScript returns the script version associated with the address along
// with a script to pay a transaction output to the address.
//
// This is part of the Address interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) PaymentScript() (uint16, []byte) {
// A pay-to-pubkey-hash-ecdsa-secp256k1 script is of the form:
// DUP HASH160 <20-byte hash> EQUALVERIFY CHECKSIG
var script [p2pkhPaymentScriptLen]byte
addr.putPaymentScript(script[:])
return 0, script[:]
}
// VotingRightsScript returns the script version associated with the address
// along with a script to give voting rights to the address. It is only valid
// when used in stake ticket purchase transactions.
//
// This is part of the StakeAddress interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) VotingRightsScript() (uint16, []byte) {
// A script that assigns voting rights for a ticket to this address type is
// of the form:
// SSTX [standard pay-to-pubkey-hash-ecdsa-secp256k1 script]
var script [p2pkhPaymentScriptLen + 1]byte
script[0] = opSSTx
addr.putPaymentScript(script[1:])
return 0, script[:]
}
// RewardCommitmentScript returns the script version associated with the address
// along with a script that commits the original funds locked to purchase a
// ticket plus the reward to the address along with limits to impose on any
// fees.
//
// This is part of the StakeAddress interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) RewardCommitmentScript(amount int64, limits uint16) (uint16, []byte) {
// The reward commitment output of a ticket purchase is a provably pruneable
// script of the form:
// RETURN <20-byte hash || 8-byte amount || 2-byte fee limits>
//
// The high bit of the amount is used to indicate whether the provided hash
// is a public key hash that represents a pay-to-pubkey-hash-ecdsa-secp256k1
// script or a script hash that represents a pay-to-script-hash script. It
// is NOT set for a public key hash.
var script [32]byte
script[0] = opReturn
script[1] = opData30
copy(script[2:22], addr.hash[:])
binary.LittleEndian.PutUint64(script[22:30], uint64(amount) & ^commitP2SHFlag)
binary.LittleEndian.PutUint16(script[30:32], limits)
return 0, script[:]
}
// StakeChangeScript returns the script version associated with the address
// along with a script to pay change to the address. It is only valid when used
// in stake ticket purchase and treasury add transactions.
//
// This is part of the StakeAddress interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) StakeChangeScript() (uint16, []byte) {
// A stake change script to this address type is of the form:
// SSTXCHANGE [standard pay-to-pubkey-hash-ecdsa-secp256k1 script]
var script [p2pkhPaymentScriptLen + 1]byte
script[0] = opSSTxChange
addr.putPaymentScript(script[1:])
return 0, script[:]
}
// PayVoteCommitmentScript returns the script version associated with the
// address along with a script to pay the original funds locked to purchase a
// ticket plus the reward to the address. The address must have previously been
// committed to by the ticket purchase. The script is only valid when used in
// stake vote transactions whose associated tickets are eligible to vote.
//
// This is part of the StakeAddress interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) PayVoteCommitmentScript() (uint16, []byte) {
// A script that pays a ticket commitment as part of a vote to this address
// type is of the form:
// SSGEN [standard pay-to-pubkey-hash-ecdsa-secp256k1 script]
var script [p2pkhPaymentScriptLen + 1]byte
script[0] = opSSGen
addr.putPaymentScript(script[1:])
return 0, script[:]
}
// PayRevokeCommitmentScript returns the script version associated with the
// address along with a script to revoke an expired or missed ticket which pays
// the original funds locked to purchase a ticket to the address. The address
// must have previously been committed to by the ticket purchase. The script is
// only valid when used in stake revocation transactions whose associated
// tickets have been missed or expired.
//
// This is part of the StakeAddress interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) PayRevokeCommitmentScript() (uint16, []byte) {
// A ticket revocation script to this address type is of the form:
// SSRTX [standard pay-to-pubkey-hash-ecdsa-secp256k1 script]
var script [p2pkhPaymentScriptLen + 1]byte
script[0] = opSSRTx
addr.putPaymentScript(script[1:])
return 0, script[:]
}
// PayFromTreasuryScript returns the script version associated with the address
// along with a script that pays funds from the treasury to the address. The
// script is only valid when used in treasury spend transactions.
//
// This is part of the StakeAddress interface implementation.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) PayFromTreasuryScript() (uint16, []byte) {
// A script that pays from the treasury as a part of a treasury spend to
// this address type is of the form:
// TGEN [standard pay-to-pubkey-hash-ecdsa-secp256k1 script]
var script [p2pkhPaymentScriptLen + 1]byte
script[0] = opTGen
addr.putPaymentScript(script[1:])
return 0, script[:]
}
// Hash160 returns the underlying array of the pubkey hash. This can be useful
// when an array is more appropriate than a slice (for example, when used as map
// keys).
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) Hash160() *[ripemd160.Size]byte {
return &addr.hash
}
// String returns a human-readable string for the address.
//
// This is equivalent to calling Address, but is provided so the type can be
// used as a fmt.Stringer.
func (addr *AddressPubKeyHashEcdsaSecp256k1V0) String() string {
return addr.Address()
}
// DecodeAddressV0 decodes the string encoding of an address and returns the
// relevant Address if it is a valid encoding for a known version 0 address type
// and is for the network identified by the provided parameters.

View File

@ -35,6 +35,10 @@ const (
// ErrInvalidPubKeyFormat indicates that a serialized public key parsed
// successfully, but is not one of the allowed formats.
ErrInvalidPubKeyFormat = ErrorKind("ErrInvalidPubKeyFormat")
// ErrInvalidHashLen indicates that either a public key hash or a script
// hash is not an allowed length.
ErrInvalidHashLen = ErrorKind("ErrInvalidHashLen")
)
// Error satisfies the error interface and prints human-readable errors.

View File

@ -23,6 +23,7 @@ func TestErrorKindStringer(t *testing.T) {
{ErrBadAddressChecksum, "ErrBadAddressChecksum"},
{ErrInvalidPubKey, "ErrInvalidPubKey"},
{ErrInvalidPubKeyFormat, "ErrInvalidPubKeyFormat"},
{ErrInvalidHashLen, "ErrInvalidHashLen"},
}
for i, test := range tests {