From c6d7cd71ac2575f15b1d4ea7026265db7464c45d Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 5 Mar 2021 08:34:15 -0600 Subject: [PATCH] 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. --- go.mod | 1 + internal/staging/stdaddr/address.go | 7 + internal/staging/stdaddr/address_test.go | 120 +++++++++++ internal/staging/stdaddr/addressv0.go | 260 ++++++++++++++++++++++- internal/staging/stdaddr/error.go | 4 + internal/staging/stdaddr/error_test.go | 1 + 6 files changed, 392 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 96fa6d19..c908c412 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/staging/stdaddr/address.go b/internal/staging/stdaddr/address.go index d97f8b29..fed1ed40 100644 --- a/internal/staging/stdaddr/address.go +++ b/internal/staging/stdaddr/address.go @@ -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) diff --git a/internal/staging/stdaddr/address_test.go b/internal/staging/stdaddr/address_test.go index 76933133..cc75e62e 100644 --- a/internal/staging/stdaddr/address_test.go +++ b/internal/staging/stdaddr/address_test.go @@ -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 { diff --git a/internal/staging/stdaddr/addressv0.go b/internal/staging/stdaddr/addressv0.go index b2029ce7..477d298d 100644 --- a/internal/staging/stdaddr/addressv0.go +++ b/internal/staging/stdaddr/addressv0.go @@ -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. diff --git a/internal/staging/stdaddr/error.go b/internal/staging/stdaddr/error.go index e7a51032..dce251c6 100644 --- a/internal/staging/stdaddr/error.go +++ b/internal/staging/stdaddr/error.go @@ -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. diff --git a/internal/staging/stdaddr/error_test.go b/internal/staging/stdaddr/error_test.go index ed688bf3..aacf01e5 100644 --- a/internal/staging/stdaddr/error_test.go +++ b/internal/staging/stdaddr/error_test.go @@ -23,6 +23,7 @@ func TestErrorKindStringer(t *testing.T) { {ErrBadAddressChecksum, "ErrBadAddressChecksum"}, {ErrInvalidPubKey, "ErrInvalidPubKey"}, {ErrInvalidPubKeyFormat, "ErrInvalidPubKeyFormat"}, + {ErrInvalidHashLen, "ErrInvalidHashLen"}, } for i, test := range tests {