stdaddr: Add infrastructure for v0 decoding.
This adds the overall infrastracture for decoding version 0 addresses along with associated testing infrastructure. Note that, in order to help ease the review process, this only adds the relevant infrastructure and does not yet support any version 0 address types. Each supported version 0 address type will be added in future commits. This is part of a series of commits to fully implement the stdaddr package.
This commit is contained in:
parent
2b4557f56e
commit
67f89a7eed
@ -16,6 +16,7 @@ import (
|
||||
// required when encoding and decoding addresses. These values are typically
|
||||
// well-defined and unique per network.
|
||||
type AddressParams interface {
|
||||
AddressParamsV0
|
||||
}
|
||||
|
||||
// Address represents any type of destination a transaction output may spend to.
|
||||
@ -324,11 +325,42 @@ func NewAddressScriptHash(scriptVersion uint16, redeemScript []byte,
|
||||
return nil, makeError(ErrUnsupportedScriptVersion, str)
|
||||
}
|
||||
|
||||
// probablyV0Base58Addr returns true when the provided string looks like a
|
||||
// version 0 base58 address as determined by their length and only containing
|
||||
// runes in the base58 alphabet used by Decred for version 0 addresses.
|
||||
func probablyV0Base58Addr(s string) bool {
|
||||
// Ensure the length is one of the possible values for supported version 0
|
||||
// addresses.
|
||||
if len(s) != 35 && len(s) != 53 {
|
||||
return false
|
||||
}
|
||||
|
||||
// The modified base58 alphabet used by Decred for version 0 addresses is:
|
||||
// 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
|
||||
for _, r := range s {
|
||||
if r < '1' || r > 'z' ||
|
||||
r == 'I' || r == 'O' || r == 'l' ||
|
||||
(r > '9' && r < 'A') || (r > 'Z' && r < 'a') {
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Parsing code for future address/script versions should be added as the
|
||||
// most recent case in the switch statement. The expectation is that newer
|
||||
// version addresses will become more common, so they should be checked
|
||||
// first.
|
||||
switch {
|
||||
case probablyV0Base58Addr(addr):
|
||||
return DecodeAddressV0(addr, params)
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("address %q is not a supported type", addr)
|
||||
return nil, makeError(ErrUnsupportedAddress, str)
|
||||
|
||||
457
internal/staging/stdaddr/address_test.go
Normal file
457
internal/staging/stdaddr/address_test.go
Normal file
@ -0,0 +1,457 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/decred/base58"
|
||||
"github.com/decred/dcrd/crypto/ripemd160"
|
||||
)
|
||||
|
||||
// mockAddrParams implements the AddressParams interface and is used throughout
|
||||
// the tests to mock multiple networks.
|
||||
type mockAddrParams struct {
|
||||
pubKeyID [2]byte
|
||||
pkhEcdsaID [2]byte
|
||||
pkhEd25519ID [2]byte
|
||||
pkhSchnorrID [2]byte
|
||||
scriptHashID [2]byte
|
||||
privKeyID [2]byte
|
||||
}
|
||||
|
||||
// AddrIDPubKeyV0 returns the magic prefix bytes associated with the mock params
|
||||
// for version 0 pay-to-pubkey addresses.
|
||||
//
|
||||
// This is part of the AddressParams interface.
|
||||
func (p *mockAddrParams) AddrIDPubKeyV0() [2]byte {
|
||||
return p.pubKeyID
|
||||
}
|
||||
|
||||
// AddrIDPubKeyHashECDSAV0 returns the magic prefix bytes associated with the
|
||||
// mock params for version 0 pay-to-pubkey-hash addresses where the underlying
|
||||
// pubkey is secp256k1 and the signature algorithm is ECDSA.
|
||||
//
|
||||
// This is part of the AddressParams interface.
|
||||
func (p *mockAddrParams) AddrIDPubKeyHashECDSAV0() [2]byte {
|
||||
return p.pkhEcdsaID
|
||||
}
|
||||
|
||||
// AddrIDPubKeyHashEd25519V0 returns the magic prefix bytes associated with the
|
||||
// mock params for version 0 pay-to-pubkey-hash addresses where the underlying
|
||||
// pubkey and signature algorithm are Ed25519.
|
||||
//
|
||||
// This is part of the AddressParams interface.
|
||||
func (p *mockAddrParams) AddrIDPubKeyHashEd25519V0() [2]byte {
|
||||
return p.pkhEd25519ID
|
||||
}
|
||||
|
||||
// AddrIDPubKeyHashSchnorrV0 returns the magic prefix bytes associated with the
|
||||
// mock params for version 0 pay-to-pubkey-hash addresses where the underlying
|
||||
// pubkey is secp256k1 and the signature algorithm is Schnorr.
|
||||
//
|
||||
// This is part of the AddressParams interface.
|
||||
func (p *mockAddrParams) AddrIDPubKeyHashSchnorrV0() [2]byte {
|
||||
return p.pkhSchnorrID
|
||||
}
|
||||
|
||||
// AddrIDScriptHashV0 returns the magic prefix bytes associated with the mock
|
||||
// params for version 0 pay-to-script-hash addresses.
|
||||
//
|
||||
// This is part of the AddressParams interface.
|
||||
func (p *mockAddrParams) AddrIDScriptHashV0() [2]byte {
|
||||
return p.scriptHashID
|
||||
}
|
||||
|
||||
// mockMainNetParams returns mock mainnet address parameters to use throughout
|
||||
// the tests. They match the Decred mainnet params as of the time this comment
|
||||
// was written.
|
||||
func mockMainNetParams() *mockAddrParams {
|
||||
return &mockAddrParams{
|
||||
pubKeyID: [2]byte{0x13, 0x86}, // starts with Dk
|
||||
pkhEcdsaID: [2]byte{0x07, 0x3f}, // starts with Ds
|
||||
pkhEd25519ID: [2]byte{0x07, 0x1f}, // starts with De
|
||||
pkhSchnorrID: [2]byte{0x07, 0x01}, // starts with DS
|
||||
scriptHashID: [2]byte{0x07, 0x1a}, // starts with Dc
|
||||
privKeyID: [2]byte{0x22, 0xde}, // starts with Pm
|
||||
}
|
||||
}
|
||||
|
||||
// mockTestNetParams returns mock testnet address parameters to use throughout
|
||||
// the tests. They match the Decred testnet params as of the time this comment
|
||||
// was written.
|
||||
func mockTestNetParams() *mockAddrParams {
|
||||
return &mockAddrParams{
|
||||
pubKeyID: [2]byte{0x28, 0xf7}, // starts with Tk
|
||||
pkhEcdsaID: [2]byte{0x0f, 0x21}, // starts with Ts
|
||||
pkhEd25519ID: [2]byte{0x0f, 0x01}, // starts with Te
|
||||
pkhSchnorrID: [2]byte{0x0e, 0xe3}, // starts with TS
|
||||
scriptHashID: [2]byte{0x0e, 0xfc}, // starts with Tc
|
||||
privKeyID: [2]byte{0x23, 0x0e}, // starts with Pt
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddresses ensures that address-related APIs work as intended including
|
||||
// that they are properly encoded and decoded, that they produce the expected
|
||||
// payment-related scripts, and that error paths fail as expected. For
|
||||
// addresses that implement the stake address interface, the stake-related
|
||||
// scripts are also tested.
|
||||
func TestAddresses(t *testing.T) {
|
||||
mainNetParams := mockMainNetParams()
|
||||
testNetParams := mockTestNetParams()
|
||||
|
||||
type newAddrFn func() (Address, error)
|
||||
tests := []struct {
|
||||
name string // test description
|
||||
makeAddr newAddrFn // function to construct new address via API
|
||||
makeErr error // expected error from new address function
|
||||
addr string // expected address and address to decode
|
||||
net AddressParams // params for network
|
||||
decodeErr error // expected error from decode
|
||||
version uint16 // expected scripts version
|
||||
payScript string // hex-encoded expected payment script
|
||||
voteScript string // hex-encoded expected voting rights script
|
||||
rewardAmount int64 // reward commitment amount
|
||||
feeLimits uint16 // reward fee limits commitment
|
||||
rewardScript string // hex-encoded expected reward commitment script
|
||||
changeScript string // hex-encoded expected stake change script
|
||||
commitScript string // hex-encoded expected vote commitment script
|
||||
revokeScript string // hex-encoded expected revoke commitment script
|
||||
trsyScript string // hex-encoded expected pay from treasury script
|
||||
}{{
|
||||
// ---------------------------------------------------------------------
|
||||
// Misc decoding error tests.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
name: "bad checksum",
|
||||
addr: "TsmWaPM77WSyA3aiQ2Q1KnwGDVWvEkhip23",
|
||||
net: testNetParams,
|
||||
decodeErr: ErrBadAddressChecksum,
|
||||
}, {
|
||||
name: "parse valid mainnet address with testnet rejected",
|
||||
addr: "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu",
|
||||
net: testNetParams,
|
||||
decodeErr: ErrUnsupportedAddress,
|
||||
}, {
|
||||
name: "mainnet p2pk with no data for pubkey",
|
||||
addr: "Aiz5jz1s",
|
||||
net: mainNetParams,
|
||||
decodeErr: ErrUnsupportedAddress,
|
||||
}, {
|
||||
name: "invalid base58 (l not in base58 alphabet)",
|
||||
addr: "DsUZxxoHlSty8DCfwfartwTYbuhmVct7tJu",
|
||||
net: mainNetParams,
|
||||
decodeErr: ErrUnsupportedAddress,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
// Create address from test constructor and ensure it produces the
|
||||
// expected encoded address when the constructor is specified.
|
||||
if test.makeAddr != nil {
|
||||
addr, err := test.makeAddr()
|
||||
if !errors.Is(err, test.makeErr) {
|
||||
t.Errorf("%s: mismatched err -- got %v, want %v", test.name, err,
|
||||
test.makeErr)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure encoding the address is the same as the original.
|
||||
encoded := addr.Address()
|
||||
if encoded != test.addr {
|
||||
t.Errorf("%s: unexpected address -- got %v, want %v", test.name,
|
||||
encoded, test.addr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Decode address and ensure the expected error is received.
|
||||
decodedAddr, err := DecodeAddress(test.addr, test.net)
|
||||
if !errors.Is(err, test.decodeErr) {
|
||||
t.Errorf("%s: mismatched err -- got %v, want %v", test.name, err,
|
||||
test.decodeErr)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the payment script version and contents are the expected
|
||||
// values.
|
||||
wantPayScript, err := hex.DecodeString(test.payScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotPayScriptVersion, gotPayScript := decodedAddr.PaymentScript()
|
||||
if gotPayScriptVersion != test.version {
|
||||
t.Errorf("%s: mismatched payment script version -- got %d, want %d",
|
||||
test.name, gotPayScriptVersion, test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotPayScript, wantPayScript) {
|
||||
t.Errorf("%s: mismatched payment script -- got %x, want %x",
|
||||
test.name, gotPayScript, wantPayScript)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure stake-specific interface results produce the expected values.
|
||||
if stakeAddr, ok := decodedAddr.(StakeAddress); ok {
|
||||
// Ensure the voting rights script version and contents are the
|
||||
// expected values.
|
||||
wantScript, err := hex.DecodeString(test.voteScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotScriptVer, gotScript := stakeAddr.VotingRightsScript()
|
||||
if gotScriptVer != test.version {
|
||||
t.Errorf("%s: mismatched voting rights script version -- got "+
|
||||
"%d, want %d", test.name, gotScriptVer,
|
||||
test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotScript, wantScript) {
|
||||
t.Errorf("%s: mismatched voting rights script -- got %x, want %x",
|
||||
test.name, gotScript, wantScript)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the reward commitment script version and contents are the
|
||||
// expected values.
|
||||
wantScript, err = hex.DecodeString(test.rewardScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotScriptVer, gotScript = stakeAddr.RewardCommitmentScript(
|
||||
test.rewardAmount, test.feeLimits)
|
||||
if gotScriptVer != test.version {
|
||||
t.Errorf("%s: mismatched reward cmt script version -- got %d, "+
|
||||
"want %d", test.name, gotScriptVer, test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotScript, wantScript) {
|
||||
t.Errorf("%s: mismatched reward cmt script -- got %x, want %x",
|
||||
test.name, gotScript, wantScript)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the stake change script version and contents are the
|
||||
// expected values.
|
||||
wantScript, err = hex.DecodeString(test.changeScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotScriptVer, gotScript = stakeAddr.StakeChangeScript()
|
||||
if gotScriptVer != test.version {
|
||||
t.Errorf("%s: mismatched change script version -- got %d, "+
|
||||
"want %d", test.name, gotScriptVer, test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotScript, wantScript) {
|
||||
t.Errorf("%s: mismatched change script -- got %x, want %x",
|
||||
test.name, gotScript, wantScript)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the vote commitment script version and contents are the
|
||||
// expected values.
|
||||
wantScript, err = hex.DecodeString(test.commitScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotScriptVer, gotScript = stakeAddr.PayVoteCommitmentScript()
|
||||
if gotScriptVer != test.version {
|
||||
t.Errorf("%s: mismatched vote commit script version -- got %d, "+
|
||||
"want %d", test.name, gotScriptVer, test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotScript, wantScript) {
|
||||
t.Errorf("%s: mismatched vote commit script -- got %x, want %x",
|
||||
test.name, gotScript, wantScript)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the revoke commitment script version and contents are the
|
||||
// expected values.
|
||||
wantScript, err = hex.DecodeString(test.revokeScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotScriptVer, gotScript = stakeAddr.PayRevokeCommitmentScript()
|
||||
if gotScriptVer != test.version {
|
||||
t.Errorf("%s: mismatched revoke cmt script version -- got %d, "+
|
||||
"want %d", test.name, gotScriptVer, test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotScript, wantScript) {
|
||||
t.Errorf("%s: mismatched revoke cmt script -- got %x, want %x",
|
||||
test.name, gotScript, wantScript)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the pay from treasury script version and contents are the
|
||||
// expected values.
|
||||
wantScript, err = hex.DecodeString(test.trsyScript)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected hex decode err: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
gotScriptVer, gotScript = stakeAddr.PayFromTreasuryScript()
|
||||
if gotScriptVer != test.version {
|
||||
t.Errorf("%s: mismatched treasury change script version -- "+
|
||||
"got %d, want %d", test.name, gotScriptVer, test.version)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(gotScript, wantScript) {
|
||||
t.Errorf("%s: mismatched treasury change script -- got %x, "+
|
||||
"want %x", test.name, gotScript, wantScript)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure encoding the address is the same as the original.
|
||||
encoded := decodedAddr.Address()
|
||||
if encoded != test.addr {
|
||||
t.Errorf("%s: decoding and encoding produced different addresses "+
|
||||
"-- got %v, want %v", test.name, encoded, test.addr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the stringer returns the same address as the original.
|
||||
if ds, ok := decodedAddr.(fmt.Stringer); ok && ds.String() != test.addr {
|
||||
t.Errorf("%s: mismatched decoded stringer -- got %v, want %v",
|
||||
test.name, ds.String(), test.addr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the Hash160 method for the addresses that support it returns
|
||||
// the expected value.
|
||||
if h160er, ok := decodedAddr.(Hash160er); ok {
|
||||
decodedBytes := base58.Decode(test.addr)
|
||||
wantH160 := decodedBytes[2 : 2+ripemd160.Size]
|
||||
if gotH160 := h160er.Hash160()[:]; !bytes.Equal(gotH160, wantH160) {
|
||||
t.Errorf("%s: mismatched hash160 -- got %x, want %x", test.name,
|
||||
gotH160, wantH160)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeAddressV0Corners ensures that some additional errors that are
|
||||
// specific to decoding version 0 addresses directly, as opposed to via the
|
||||
// generic API, work as intended. This is necessary because the generic address
|
||||
// decoding function contains additional logic to avoid even attempting to
|
||||
// decode addresses which can't possibly be one of the supported version 0
|
||||
// address types, while the version 0 decoding logic specifically attempts to
|
||||
// decode the address in order to provide more detailed errors.
|
||||
func TestDecodeAddressV0Corners(t *testing.T) {
|
||||
mainNetParams := mockMainNetParams()
|
||||
|
||||
tests := []struct {
|
||||
name string // test description
|
||||
addr string // expected address and address to decode
|
||||
net AddressParams // params for network
|
||||
decodeErr error // expected error from decode
|
||||
}{{
|
||||
// ---------------------------------------------------------------------
|
||||
// Misc decoding error tests.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
name: "mainnet p2pk with no data for pubkey",
|
||||
addr: "Aiz5jz1s",
|
||||
net: mainNetParams,
|
||||
decodeErr: ErrMalformedAddressData,
|
||||
}, {
|
||||
name: "invalid base58 (l not in base58 alphabet)",
|
||||
addr: "DsUZxxoHlSty8DCfwfartwTYbuhmVct7tJu",
|
||||
net: mainNetParams,
|
||||
decodeErr: ErrMalformedAddress,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
_, err := DecodeAddressV0(test.addr, test.net)
|
||||
if !errors.Is(err, test.decodeErr) {
|
||||
t.Errorf("%s: mismatched err -- got %v, want %v", test.name, err,
|
||||
test.decodeErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProbablyV0Base58Addr ensures the function that determines if an address
|
||||
// is probably a base58 address works as intended by checking off by ones and
|
||||
// ensuring all allowed characters in the modified base58 alphabet are accepted.
|
||||
func TestProbablyV0Base58Addr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string // test description
|
||||
str string // string to test
|
||||
want bool // expected result
|
||||
}{{
|
||||
name: "all allowed base58 chars part 1",
|
||||
str: "123456789ABCDEFGHJKLMNPQRSTUVWXYZab",
|
||||
want: true,
|
||||
}, {
|
||||
name: "all allowed base58 chars part 2",
|
||||
str: "QRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
|
||||
want: true,
|
||||
}, {
|
||||
name: "invalid base58 (0 not in base58 alphabet, one less than '1')",
|
||||
str: "DsUZxxoH0Sty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 ({ not in base58 alphabet, one more than 'z')",
|
||||
str: "DsUZxxoH{Sty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 (I not in base58 alphabet)",
|
||||
str: "DsUZxxoHISty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 (O not in base58 alphabet)",
|
||||
str: "DsUZxxoHOSty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 (l not in base58 alphabet)",
|
||||
str: "DsUZxxoHlSty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 (: not in base58 alphabet, one more than '9')",
|
||||
str: "DsUZxxoH:Sty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 (@ not in base58 alphabet, one less than 'A')",
|
||||
str: "DsUZxxoH@Sty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 ([ not in base58 alphabet, one more than 'Z')",
|
||||
str: "DsUZxxoH[Sty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}, {
|
||||
name: "invalid base58 (` not in base58 alphabet, one less than 'a')",
|
||||
str: "DsUZxxoH`Sty8DCfwfartwTYbuhmVct7tJu",
|
||||
want: false,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
got := probablyV0Base58Addr(test.str)
|
||||
if got != test.want {
|
||||
t.Errorf("%q: unexpected result -- got %v, want %v", test.name, got,
|
||||
test.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
158
internal/staging/stdaddr/addressv0.go
Normal file
158
internal/staging/stdaddr/addressv0.go
Normal file
@ -0,0 +1,158 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
|
||||
"github.com/decred/base58"
|
||||
"github.com/decred/dcrd/dcrec"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
// sigTypeSecp256k1PubKeyCompOddFlag specifies the bitmask to apply to the
|
||||
// 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)
|
||||
)
|
||||
|
||||
// AddressParamsV0 defines an interface that is used to provide the parameters
|
||||
// required when encoding and decoding addresses for version 0 scripts. These
|
||||
// values are typically well-defined and unique per network.
|
||||
type AddressParamsV0 interface {
|
||||
// AddrIDPubKeyV0 returns the magic prefix bytes for version 0 pay-to-pubkey
|
||||
// addresses.
|
||||
AddrIDPubKeyV0() [2]byte
|
||||
|
||||
// AddrIDPubKeyHashECDSAV0 returns the magic prefix bytes for version 0
|
||||
// pay-to-pubkey-hash addresses where the underlying pubkey is secp256k1 and
|
||||
// the signature algorithm is ECDSA.
|
||||
AddrIDPubKeyHashECDSAV0() [2]byte
|
||||
|
||||
// AddrIDPubKeyHashEd25519V0 returns the magic prefix bytes for version 0
|
||||
// pay-to-pubkey-hash addresses where the underlying pubkey and signature
|
||||
// algorithm are Ed25519.
|
||||
AddrIDPubKeyHashEd25519V0() [2]byte
|
||||
|
||||
// AddrIDPubKeyHashSchnorrV0 returns the magic prefix bytes for version 0
|
||||
// pay-to-pubkey-hash addresses where the underlying pubkey is secp256k1 and
|
||||
// the signature algorithm is Schnorr.
|
||||
AddrIDPubKeyHashSchnorrV0() [2]byte
|
||||
|
||||
// AddrIDScriptHashV0 returns the magic prefix bytes for version 0
|
||||
// pay-to-script-hash addresses.
|
||||
AddrIDScriptHashV0() [2]byte
|
||||
}
|
||||
|
||||
// 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.
|
||||
func DecodeAddressV0(addr string, params AddressParamsV0) (Address, error) {
|
||||
// Attempt to decode the address and address type.
|
||||
decoded, addrID, err := base58.CheckDecode(addr)
|
||||
if err != nil {
|
||||
kind := ErrMalformedAddress
|
||||
if errors.Is(err, base58.ErrChecksum) {
|
||||
kind = ErrBadAddressChecksum
|
||||
}
|
||||
str := fmt.Sprintf("failed to decoded address %q: %v", addr, err)
|
||||
return nil, makeError(kind, str)
|
||||
}
|
||||
|
||||
// Decode the address according to the address type.
|
||||
switch addrID {
|
||||
case params.AddrIDScriptHashV0():
|
||||
return NewAddressScriptHashFromHash(0, decoded, params)
|
||||
|
||||
case params.AddrIDPubKeyHashECDSAV0():
|
||||
return NewAddressPubKeyHashEcdsaSecp256k1(0, decoded, params)
|
||||
|
||||
case params.AddrIDPubKeyHashSchnorrV0():
|
||||
return NewAddressPubKeyHashSchnorrSecp256k1(0, decoded, params)
|
||||
|
||||
case params.AddrIDPubKeyHashEd25519V0():
|
||||
return NewAddressPubKeyHashEd25519(0, decoded, params)
|
||||
|
||||
case params.AddrIDPubKeyV0():
|
||||
// Ensure the decoded data has the expected signature type identifier
|
||||
// byte.
|
||||
if len(decoded) < 1 {
|
||||
str := fmt.Sprintf("address %q decoded data is empty", addr)
|
||||
return nil, makeError(ErrMalformedAddressData, str)
|
||||
}
|
||||
|
||||
// Decode according to the crypto algorithm and signature scheme.
|
||||
sigType := decoded[0] & ^sigTypeSecp256k1PubKeyCompOddFlag
|
||||
switch dcrec.SignatureType(sigType) {
|
||||
case dcrec.STEcdsaSecp256k1:
|
||||
// The encoded data for this case is the 32-byte X coordinate for a
|
||||
// secp256k1 public key along with the oddness of the Y coordinate
|
||||
// encoded via the high bit of the first byte.
|
||||
//
|
||||
// Reconstruct the standard compressed serialized public key format
|
||||
// by choosing the correct prefix byte depending on the encoded
|
||||
// Y-coordinate oddness pass it along to the constructor of the
|
||||
// appropriate type to validate and return the relevant address
|
||||
// instance.
|
||||
const reqPubKeyLen = 33
|
||||
if len(decoded) != reqPubKeyLen {
|
||||
str := fmt.Sprintf("public key is %d bytes vs required %d bytes",
|
||||
len(decoded), reqPubKeyLen)
|
||||
return nil, makeError(ErrMalformedAddressData, str)
|
||||
}
|
||||
isOddY := decoded[0]&sigTypeSecp256k1PubKeyCompOddFlag != 0
|
||||
prefix := secp256k1.PubKeyFormatCompressedEven
|
||||
if isOddY {
|
||||
prefix = secp256k1.PubKeyFormatCompressedOdd
|
||||
}
|
||||
decoded[0] = prefix
|
||||
return NewAddressPubKeyEcdsaSecp256k1Raw(0, decoded, params)
|
||||
|
||||
case dcrec.STEd25519:
|
||||
const reqPubKeyLen = 32
|
||||
pubKey := decoded[1:]
|
||||
if len(pubKey) != reqPubKeyLen {
|
||||
str := fmt.Sprintf("public key is %d bytes vs required %d bytes",
|
||||
len(pubKey), reqPubKeyLen)
|
||||
return nil, makeError(ErrMalformedAddressData, str)
|
||||
}
|
||||
|
||||
// The encoded data for this case is the actual Ed25519 public key,
|
||||
// so just pass it along unaltered to the constructor of the
|
||||
// appropriate type to validate and return the relevant address
|
||||
// instance.
|
||||
return NewAddressPubKeyEd25519Raw(0, pubKey, params)
|
||||
|
||||
case dcrec.STSchnorrSecp256k1:
|
||||
// The encoded data for this case is the 32-byte X coordinate for a
|
||||
// secp256k1 public key along with the oddness of the Y coordinate
|
||||
// encoded via the high bit of the first byte.
|
||||
//
|
||||
// Reconstruct the standard compressed serialized public key format
|
||||
// by choosing the correct prefix byte depending on the encoded
|
||||
// Y-coordinate oddness pass it along to the constructor of the
|
||||
// appropriate type to validate and return the relevant address
|
||||
// instance.
|
||||
const reqPubKeyLen = 33
|
||||
if len(decoded) != reqPubKeyLen {
|
||||
str := fmt.Sprintf("public key is %d bytes vs required %d bytes",
|
||||
len(decoded), reqPubKeyLen)
|
||||
return nil, makeError(ErrMalformedAddressData, str)
|
||||
}
|
||||
isOddY := decoded[0]&sigTypeSecp256k1PubKeyCompOddFlag != 0
|
||||
prefix := secp256k1.PubKeyFormatCompressedEven
|
||||
if isOddY {
|
||||
prefix = secp256k1.PubKeyFormatCompressedOdd
|
||||
}
|
||||
decoded[0] = prefix
|
||||
return NewAddressPubKeySchnorrSecp256k1Raw(0, decoded, params)
|
||||
}
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("address %q is not a supported type", addr)
|
||||
return nil, makeError(ErrUnsupportedAddress, str)
|
||||
}
|
||||
@ -16,6 +16,17 @@ const (
|
||||
// ErrUnsupportedScriptVersion indicates that an address type does not
|
||||
// support a given script version.
|
||||
ErrUnsupportedScriptVersion = ErrorKind("ErrUnsupportedScriptVersion")
|
||||
|
||||
// ErrMalformedAddress indicates an address failed to decode.
|
||||
ErrMalformedAddress = ErrorKind("ErrMalformedAddress")
|
||||
|
||||
// ErrMalformedAddressData indicates an address successfully decoded and is
|
||||
// a recognized type, but the encoded data is not the expected length.
|
||||
ErrMalformedAddressData = ErrorKind("ErrMalformedAddressData")
|
||||
|
||||
// ErrBadAddressChecksum indicates an address failed to decode due to an
|
||||
// invalid checksum.
|
||||
ErrBadAddressChecksum = ErrorKind("ErrBadAddressChecksum")
|
||||
)
|
||||
|
||||
// Error satisfies the error interface and prints human-readable errors.
|
||||
|
||||
@ -18,6 +18,9 @@ func TestErrorKindStringer(t *testing.T) {
|
||||
}{
|
||||
{ErrUnsupportedAddress, "ErrUnsupportedAddress"},
|
||||
{ErrUnsupportedScriptVersion, "ErrUnsupportedScriptVersion"},
|
||||
{ErrMalformedAddress, "ErrMalformedAddress"},
|
||||
{ErrMalformedAddressData, "ErrMalformedAddressData"},
|
||||
{ErrBadAddressChecksum, "ErrBadAddressChecksum"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user