From 2b4557f56e92273ade52c8fdbef52804398489a1 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 5 Mar 2021 08:34:10 -0600 Subject: [PATCH] stdaddr: Introduce package infra for std addrs. The current code for handling standard addresses implemented in dcrutil was written many years ago prior a wide variety of changes and several new features added by Decred. As a result, it entirely lacks support for some features and supports others in a roundabout and non-intuitive way. Specifically, it does not support or provide a clean path to enable support for different script versions and the way they are handled in stake transactions is entirely non-intuitive. Further, back when the original address code was implemented, it was necessary to implement script creation in the txscript package which led to the current design of providing methods such as txscript.PayTo{AddrScript,SStx,SStxChange}, and others, which need to type assert the specific concrete types of addresses in order to produce the necessary scripts. This, unfortunately, effectively negates the use of an interface to support generic addresses because it means callers, such as dcrwallet, are not able to implement their own types without somewhat invisibly breaking the script creation. Finally, the aforementioned blending of the address code into txscript has led to confusion regarding what is considered standard and what is considered consensus which has tripped up several contributors over the years. This is part of a series of commits that aims to resolve all of the aforementioned issues by introducing a new package named stdaddr which entirely reworks the way addresses are handled. For the time being, the package is introduced into the internal staging area for initial review. The following provides an overview of some of the key features of the new design: - Supports versioned addresses - Produces scripts directly via methods implemented on underlying types - Provides direct support for creation of the scripts necessary for the staking system - Uses a capabilities-based approach via interfaces so callers can cleanly and generically determine under what circumstances addresses can be used - Allows callers to create their own concrete address types without worrying about breaking the existing ones - Clearly denotes that addresses are a standardized construction that must not be used directly in consensus code In order to help ease the review process, this commit only contains the overall generic infrastructure without adding support for any specific address types. Each supported version 0 address type will be added in future commits. --- internal/staging/stdaddr/address.go | 335 +++++++++++++++++++++++++ internal/staging/stdaddr/error.go | 48 ++++ internal/staging/stdaddr/error_test.go | 131 ++++++++++ 3 files changed, 514 insertions(+) create mode 100644 internal/staging/stdaddr/address.go create mode 100644 internal/staging/stdaddr/error.go create mode 100644 internal/staging/stdaddr/error_test.go diff --git a/internal/staging/stdaddr/address.go b/internal/staging/stdaddr/address.go new file mode 100644 index 00000000..1eef1125 --- /dev/null +++ b/internal/staging/stdaddr/address.go @@ -0,0 +1,335 @@ +// Copyright (c) 2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// Package stdaddr provides facilities for working with human-readable Decred +// payment addresses. +package stdaddr + +import ( + "fmt" + + "github.com/decred/dcrd/crypto/ripemd160" +) + +// AddressParams defines an interface that is used to provide the parameters +// required when encoding and decoding addresses. These values are typically +// well-defined and unique per network. +type AddressParams interface { +} + +// Address represents any type of destination a transaction output may spend to. +// Some examples include pay-to-pubkey (P2PK), pay-to-pubkey-hash (P2PKH), and +// pay-to-script-hash (P2SH). Address is designed to be generic enough that +// other kinds of addresses may be added in the future without changing the +// decoding and encoding API. +type Address interface { + // Address returns the string encoding of the payment address for the + // associated script version and payment script. + Address() string + + // PaymentScript returns the script version associated with the address + // along with a script to pay a transaction output to the address. + PaymentScript() (uint16, []byte) +} + +// StakeAddress is an interface for generating the specialized scripts that are +// used in the staking system. Only specific address types are supported by the +// staking system and therefore only those address types will implement this +// interface. +// +// Version 1 and 3 staking transactions only support version 0 scripts and +// address types of AddressPubKeyHashEcdsaSecp256k1V0 and AddressScriptHashV0. +// +// Callers can programmatically assert a specific address implements this +// interface to access the methods. +type StakeAddress interface { + Address + + // 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. + VotingRightsScript() (uint16, []byte) + + // 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. + RewardCommitmentScript(amount int64, limits uint16) (uint16, []byte) + + // 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. + StakeChangeScript() (uint16, []byte) + + // 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. + PayVoteCommitmentScript() (uint16, []byte) + + // 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 have expired. + PayRevokeCommitmentScript() (uint16, []byte) + + // 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. + PayFromTreasuryScript() (uint16, []byte) +} + +// AddressPubKeyHasher is an interface for public key addresses that can be +// converted to an address that imposes an encumbrance that requires the public +// key that hashes to a given public key hash along with a valid signature for +// that public key. +// +// The specific public key and signature types are dependent on the original +// address. +type AddressPubKeyHasher interface { + AddressPubKeyHash() Address +} + +// Hash160er is an interface that allows the RIPEMD-160 hash to be obtained from +// addresses that involve them. +type Hash160er interface { + Hash160() *[ripemd160.Size]byte +} + +// Secp256k1PublicKey is an interface that represents a secp256k1 public key for +// use in creating pay-to-pubkey addresses that involve them. +type Secp256k1PublicKey interface { + // SerializeCompressed serializes a public key in the 33-byte compressed + // format. + SerializeCompressed() []byte +} + +// NewAddressPubKeyEcdsaSecp256k1Raw returns an address that represents a +// payment destination which imposes an encumbrance that requires a valid ECDSA +// signature for a specific secp256k1 public key. +// +// The provided public key MUST be a valid secp256k1 public key serialized in +// the _compressed_ format or an error will be returned. +// +// See NewAddressPubKeyEcdsaSecp256k1 for a variant that accepts the public +// key as a concrete type instance instead. +// +// This function can be useful to callers who already need the serialized public +// key for other purposes to avoid the need to serialize it multiple times. +func NewAddressPubKeyEcdsaSecp256k1Raw(scriptVersion uint16, + serializedPubKey []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey addresses for version %d are not supported", + scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeyEcdsaSecp256k1 returns an address that represents a +// payment destination which imposes an encumbrance that requires a valid ECDSA +// signature for a specific secp256k1 public key. +// +// See NewAddressPubKeyEcdsaSecp256k1Raw for a variant that accepts the public +// key already serialized in the _compressed_ format instead of a concrete type. +// It can be useful to callers who already need the serialized public key for +// other purposes to avoid the need to serialize it multiple times. +func NewAddressPubKeyEcdsaSecp256k1(scriptVersion uint16, + pubKey Secp256k1PublicKey, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey addresses for version %d are not supported", + scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeyEd25519Raw returns an address that represents a payment +// destination which imposes an encumbrance that requires a valid Ed25519 +// signature for a specific Ed25519 public key. +// +// See NewAddressPubKeyEd25519 for a variant that accepts the public key as a +// concrete type instance instead. +func NewAddressPubKeyEd25519Raw(scriptVersion uint16, serializedPubKey []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey addresses for version %d are not supported", + scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// Ed25519PublicKey is an interface type that represents an Ed25519 public key +// for use in creating pay-to-pubkey addresses that involve them. +type Ed25519PublicKey interface { + // Serialize serializes the public key in a 32-byte compressed little endian + // format. + Serialize() []byte +} + +// NewAddressPubKeyEd25519 returns an address that represents a payment +// destination which imposes an encumbrance that requires a valid Ed25519 +// signature for a specific Ed25519 public key. +// +// See NewAddressPubKeyEd25519Raw for a variant that accepts the public key +// already serialized instead of a concrete type. It can be useful to callers +// who already need the serialized public key for other purposes to avoid the +// need to serialize it multiple times. +func NewAddressPubKeyEd25519(scriptVersion uint16, pubKey Ed25519PublicKey, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey addresses for version %d are not supported", + scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeySchnorrSecp256k1Raw returns an address that represents a +// payment destination which imposes an encumbrance that requires a valid +// EC-Schnorr-DCR signature for a specific secp256k1 public key. +// +// The provided public key MUST be a valid secp256k1 public key serialized in +// the _compressed_ format or an error will be returned. +// +// See NewAddressPubKeySchnorrSecp256k1 for a variant that accepts the public +// key as a concrete type instance instead. +// +// This function can be useful to callers who already need the serialized public +// key for other purposes to avoid the need to serialize it multiple times. +func NewAddressPubKeySchnorrSecp256k1Raw(scriptVersion uint16, + serializedPubKey []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey addresses for version %d are not supported", + scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeySchnorrSecp256k1 returns an address that represents a payment +// destination which imposes an encumbrance that requires a valid EC-Schnorr-DCR +// signature for a specific secp256k1 public key. +// +// See NewAddressPubKeySchnorrSecp256k1Raw for a variant that accepts the public +// key already serialized in the _compressed_ format instead of a concrete type. +// It can be useful to callers who already need the serialized public key for +// other purposes to avoid the need to serialize it multiple times. +func NewAddressPubKeySchnorrSecp256k1(scriptVersion uint16, + pubKey Secp256k1PublicKey, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey addresses for version %d are not supported", + scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeyHashEcdsaSecp256k1 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. +// +// For 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 NewAddressPubKeyHashEcdsaSecp256k1(scriptVersion uint16, pkHash []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey hash addresses for version %d are not "+ + "supported", scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeyHashEd25519 returns an address that represents a a payment +// destination which imposes an encumbrance that requires an Ed25519 public key +// that hashes to the provided public key hash along with a valid Ed25519 +// signature for that public key. +// +// For version 0 scripts, the provided public key hash must be 20 bytes and be +// the Hash160 of the correct public key or it will not be redeemable with the +// expected public key because it would hash to a different value than the +// payment script generated for the provided incorrect public key hash expects. +func NewAddressPubKeyHashEd25519(scriptVersion uint16, pkHash []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey hash addresses for version %d are not "+ + "supported", scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressPubKeyHashSchnorrSecp256k1 returns an address that represents a +// payment destination which imposes an encumbrance that requires a secp256k1 +// public key in the _compressed_ format that hashes to the provided public key +// hash along with a valid EC-Schnorr-DCR signature for that public key. +// +// For 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. +// +// WARNING: It is important to note that, unlike in the case of the ECDSA +// variant of this type of address, redemption via a public key in the +// uncompressed format is NOT supported by the consensus rules for this type, so +// it is *EXTREMELY* important to ensure the provided hash is of the serialized +// public key in the compressed format or the associated coins will NOT be +// redeemable. +func NewAddressPubKeyHashSchnorrSecp256k1(scriptVersion uint16, pkHash []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("pubkey hash addresses for version %d are not "+ + "supported", scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressScriptHashFromHash returns an address that represents a payment +// destination which imposes an encumbrance that requires a script that hashes +// to the provided script hash along with all of the encumbrances that script +// itself imposes. The script is commonly referred to as a redeem script. +// +// For version 0 scripts, the provided script hash must be 20 bytes and is +// expected to be the Hash160 of the associated redeem script. +// +// See NewAddressScriptHash for a variant that accepts the redeem script instead +// of its hash. It can be used as a convenience for callers that have the +// redeem script available. +func NewAddressScriptHashFromHash(scriptVersion uint16, scriptHash []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("script hash addresses for version %d are not "+ + "supported", scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// NewAddressScriptHash returns an address that represents a payment destination +// which imposes an encumbrance that requires a script that hashes to the same +// value as the provided script along with all of the encumbrances that script +// itself imposes. The script is commonly referred to as a redeem script. +// +// See NewAddressScriptHashFromHash for a variant that accepts the hash of the +// script directly instead of the script. It can be useful to callers that +// either already have the script hash available or do not know the associated +// script. +func NewAddressScriptHash(scriptVersion uint16, redeemScript []byte, + params AddressParams) (Address, error) { + + str := fmt.Sprintf("script hash addresses for version %d are not "+ + "supported", scriptVersion) + return nil, makeError(ErrUnsupportedScriptVersion, str) +} + +// DecodeAddress decodes the string encoding of an address and returns the +// relevant Address if it is a valid encoding for a known address type and is +// for the provided network. +func DecodeAddress(addr string, params AddressParams) (Address, error) { + // Parsing code for future address/script versions goes here. + + str := fmt.Sprintf("address %q is not a supported type", addr) + return nil, makeError(ErrUnsupportedAddress, str) +} diff --git a/internal/staging/stdaddr/error.go b/internal/staging/stdaddr/error.go new file mode 100644 index 00000000..3f1e06ea --- /dev/null +++ b/internal/staging/stdaddr/error.go @@ -0,0 +1,48 @@ +// Copyright (c) 2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package stdaddr + +// ErrorKind identifies a kind of error. +type ErrorKind string + +// These constants are used to identify a specific ErrorKind. +const ( + // ErrUnsupportedAddress indicates that an address successfully decoded, but + // is not a supported/recognized type. + ErrUnsupportedAddress = ErrorKind("ErrUnsupportedAddress") + + // ErrUnsupportedScriptVersion indicates that an address type does not + // support a given script version. + ErrUnsupportedScriptVersion = ErrorKind("ErrUnsupportedScriptVersion") +) + +// Error satisfies the error interface and prints human-readable errors. +func (e ErrorKind) Error() string { + return string(e) +} + +// Error identifies an address-related error. +// +// It has full support for errors.Is and errors.As, so the caller can ascertain +// the specific reason for the error by checking the underlying error. +type Error struct { + Err error + Description string +} + +// Error satisfies the error interface and prints human-readable errors. +func (e Error) Error() string { + return e.Description +} + +// Unwrap returns the underlying wrapped error. +func (e Error) Unwrap() error { + return e.Err +} + +// makeError creates an Error given a set of arguments. +func makeError(kind ErrorKind, desc string) Error { + return Error{Err: kind, Description: desc} +} diff --git a/internal/staging/stdaddr/error_test.go b/internal/staging/stdaddr/error_test.go new file mode 100644 index 00000000..217f424f --- /dev/null +++ b/internal/staging/stdaddr/error_test.go @@ -0,0 +1,131 @@ +// Copyright (c) 2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package stdaddr + +import ( + "errors" + "io" + "testing" +) + +// TestErrorKindStringer tests the stringized output for the ErrorKind type. +func TestErrorKindStringer(t *testing.T) { + tests := []struct { + in ErrorKind + want string + }{ + {ErrUnsupportedAddress, "ErrUnsupportedAddress"}, + {ErrUnsupportedScriptVersion, "ErrUnsupportedScriptVersion"}, + } + + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("#%d: got: %s want: %s", i, result, test.want) + continue + } + } +} + +// TestError tests the error output for the Error type. +func TestError(t *testing.T) { + t.Parallel() + + tests := []struct { + in Error + want string + }{{ + Error{Description: "some error"}, + "some error", + }, { + Error{Description: "human-readable error"}, + "human-readable error", + }} + + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("#%d: got: %s want: %s", i, result, test.want) + continue + } + } +} + +// TestErrorKindIsAs ensures both ErrorKind and Error can be identified as being +// a specific error kind via errors.Is and unwrapped via errors.As. +func TestErrorKindIsAs(t *testing.T) { + tests := []struct { + name string + err error + target error + wantMatch bool + wantAs ErrorKind + }{{ + name: "ErrUnsupportedAddress == ErrUnsupportedAddress", + err: ErrUnsupportedAddress, + target: ErrUnsupportedAddress, + wantMatch: true, + wantAs: ErrUnsupportedAddress, + }, { + name: "Error.ErrUnsupportedAddress == ErrUnsupportedAddress", + err: makeError(ErrUnsupportedAddress, ""), + target: ErrUnsupportedAddress, + wantMatch: true, + wantAs: ErrUnsupportedAddress, + }, { + name: "ErrUnsupportedAddress != ErrUnsupportedScriptVersion", + err: ErrUnsupportedAddress, + target: ErrUnsupportedScriptVersion, + wantMatch: false, + wantAs: ErrUnsupportedAddress, + }, { + name: "Error.ErrUnsupportedAddress != ErrUnsupportedScriptVersion", + err: makeError(ErrUnsupportedAddress, ""), + target: ErrUnsupportedScriptVersion, + wantMatch: false, + wantAs: ErrUnsupportedAddress, + }, { + name: "ErrUnsupportedAddress != Error.ErrUnsupportedScriptVersion", + err: ErrUnsupportedAddress, + target: makeError(ErrUnsupportedScriptVersion, ""), + wantMatch: false, + wantAs: ErrUnsupportedAddress, + }, { + name: "Error.ErrUnsupportedAddress != Error.ErrUnsupportedScriptVersion", + err: makeError(ErrUnsupportedAddress, ""), + target: makeError(ErrUnsupportedScriptVersion, ""), + wantMatch: false, + wantAs: ErrUnsupportedAddress, + }, { + name: "Error.ErrUnsupportedAddress != io.EOF", + err: makeError(ErrUnsupportedAddress, ""), + target: io.EOF, + wantMatch: false, + wantAs: ErrUnsupportedAddress, + }} + + for _, test := range tests { + // Ensure the error matches or not depending on the expected result. + result := errors.Is(test.err, test.target) + if result != test.wantMatch { + t.Errorf("%s: incorrect error identification -- got %v, want %v", + test.name, result, test.wantMatch) + continue + } + + // Ensure the underlying error kind can be unwrapped is and is the + // expected kind. + var kind ErrorKind + if !errors.As(test.err, &kind) { + t.Errorf("%s: unable to unwrap to error kind", test.name) + continue + } + if kind != test.wantAs { + t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v", + test.name, kind, test.wantAs) + continue + } + } +}