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 + } + } +}