-
Notifications
You must be signed in to change notification settings - Fork 291
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
stdscript: Add script to address conversion.
As part of the ongoing effort to refactor all code dealing with standard scripts out of txscript, this adds support to stdscript for extracting the addresses from standard recognized forms of public key scripts along with comprehensive tests to ensure proper functionality. Staying consistent with the design of determining script types, the API is designed such that there is an exported function specifically for extracting addresses from version 0 scripts as well as a separate variant that accepts the script version.
- Loading branch information
Showing
4 changed files
with
1,295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// 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 stdscript provides facilities for working with standard scripts. | ||
package stdscript | ||
|
||
import "github.com/decred/dcrd/txscript/v4/stdaddr" | ||
|
||
// ExtractAddrs analyzes the passed public key script and returns the associated | ||
// script type along with any addresses associated with it when possible. | ||
// | ||
// Note that it only works for standard script types and any data such as public | ||
// which are invalid are omitted from the results. | ||
// | ||
// NOTE: Version 0 scripts are the only currently supported version. It will | ||
// always return a nonstandard script type and no addresses for other script | ||
// versions. | ||
func ExtractAddrs(scriptVersion uint16, pkScript []byte, params stdaddr.AddressParamsV0) (ScriptType, []stdaddr.Address) { | ||
switch scriptVersion { | ||
case 0: | ||
return ExtractAddrsV0(pkScript, params) | ||
} | ||
|
||
return STNonStandard, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// 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 stdscript | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/decred/dcrd/txscript/v4/stdaddr" | ||
) | ||
|
||
// addressTest describes tests for scripts that are used to ensure various | ||
// script types and address extraction is working as expected. It's defined | ||
// separately since it is intended for use in multiple shared per-version tests. | ||
type addressTest struct { | ||
name string // test description | ||
version uint16 // version of script to analyze | ||
script []byte // script to analyze | ||
params stdaddr.AddressParams // params for network | ||
wantType ScriptType // expected script type | ||
wantAddrs []string // expected extracted addresses | ||
} | ||
|
||
// TestExtractAddrs ensures a wide variety of scripts for various script | ||
// versions return the expected extracted addresses. | ||
func TestExtractAddrs(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Specify the per-version tests to include in the overall tests here. | ||
// This is done to make it easy to add independent tests for new script | ||
// versions while still testing them all through the API that accepts a | ||
// specific version versus the exported variant that is specific to a given | ||
// version per its exported name. | ||
// | ||
// NOTE: Maintainers should add tests for new script versions following the | ||
// way addressV0Tests is handled and add the resulting per-version tests | ||
// here. | ||
perVersionTests := [][]addressTest{ | ||
addressV0Tests, | ||
} | ||
|
||
// Flatten all of the per-version tests into a single set of tests. | ||
var tests []addressTest | ||
for _, bundle := range perVersionTests { | ||
tests = append(tests, bundle...) | ||
} | ||
|
||
for _, test := range tests { | ||
// Ensure that the script is considered non standard and no addresses | ||
// are returned for unsupported script versions regardless. | ||
const unsupportedScriptVer = 9999 | ||
gotType, gotAddrs := ExtractAddrs(unsupportedScriptVer, test.script, | ||
test.params) | ||
if gotType != STNonStandard { | ||
t.Errorf("%q -- unsupported script version: mismatched type -- "+ | ||
"got %s, want %s (script %x)", test.name, gotType, | ||
STNonStandard, test.script) | ||
continue | ||
} | ||
if len(gotAddrs) != 0 { | ||
t.Errorf("%q -- unsupported script version: returned addresses -- "+ | ||
"got %s, want 0 addrs (script %x)", test.name, gotAddrs, | ||
test.script) | ||
continue | ||
} | ||
|
||
// Extract the script type and addresses for the given test data. | ||
gotType, gotAddrs = ExtractAddrs(test.version, test.script, test.params) | ||
|
||
// Ensure the script type matches the expected type. | ||
if gotType != test.wantType { | ||
t.Errorf("%q: mismatched script type -- got %v, want %v", test.name, | ||
gotType, test.wantType) | ||
continue | ||
} | ||
|
||
// Ensure the addresses match the expected ones. | ||
var gotAddrsStr []string | ||
if len(gotAddrs) > 0 { | ||
gotAddrsStr = make([]string, 0, len(gotAddrs)) | ||
for _, addr := range gotAddrs { | ||
gotAddrsStr = append(gotAddrsStr, addr.String()) | ||
} | ||
} | ||
if len(gotAddrsStr) != len(test.wantAddrs) { | ||
t.Errorf("%q: mismatched number of addrs -- got %d, want %d", | ||
test.name, len(gotAddrsStr), len(test.wantAddrs)) | ||
continue | ||
} | ||
if !reflect.DeepEqual(gotAddrsStr, test.wantAddrs) { | ||
t.Errorf("%q: mismatched address result -- got %v, want %v", | ||
test.name, gotAddrsStr, test.wantAddrs) | ||
continue | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
// 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 stdscript | ||
|
||
import ( | ||
"github.com/decred/dcrd/dcrec/secp256k1/v4" | ||
"github.com/decred/dcrd/txscript/v4/stdaddr" | ||
) | ||
|
||
// addrToSlice is a convenience function that returns a slice containing the | ||
// passed address if the given error is nil and the address is NOT nil. | ||
func addrToSlice(addr stdaddr.Address, err error) []stdaddr.Address { | ||
if err != nil || addr == nil { | ||
return nil | ||
} | ||
return []stdaddr.Address{addr} | ||
} | ||
|
||
// ExtractAddrsV0 analyzes the passed version 0 public key script and returns | ||
// the associated script type along with any addresses associated with it when | ||
// possible. | ||
// | ||
// Note that it only works for standard script types and any data such as public | ||
// keys which are invalid are omitted from the results. | ||
func ExtractAddrsV0(pkScript []byte, params stdaddr.AddressParamsV0) (ScriptType, []stdaddr.Address) { | ||
// Check for pay-to-pubkey-hash-ecdsa-secp256k1 script. | ||
if h := ExtractPubKeyHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(h, params) | ||
return STPubKeyHashEcdsaSecp256k1, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for pay-to-script-hash. | ||
if h := ExtractScriptHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(h, params) | ||
return STScriptHash, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for pay-to-pubkey-hash-ed25519 script. | ||
if data := ExtractPubKeyHashEd25519V0(pkScript); data != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEd25519V0(data, params) | ||
return STPubKeyHashEd25519, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for pay-to-pubkey-hash-schnorr-secp256k1 script. | ||
if data := ExtractPubKeyHashSchnorrSecp256k1V0(pkScript); data != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashSchnorrSecp256k1V0(data, params) | ||
return STPubKeyHashSchnorrSecp256k1, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for pay-to-pubkey script. | ||
if data := ExtractPubKeyV0(pkScript); data != nil { | ||
// Note that this parse is done because the address is intentionally | ||
// limited to compressed pubkeys, but consensus technically allows both | ||
// compressed and uncompressed pubkeys for the underlying script. | ||
var addrs []stdaddr.Address | ||
pk, err := secp256k1.ParsePubKey(data) | ||
if err == nil { | ||
addr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0(pk, params) | ||
addrs = addrToSlice(addr, err) | ||
} | ||
return STPubKeyEcdsaSecp256k1, addrs | ||
} | ||
|
||
// Check for pay-to-pubkey-ed25519 script. | ||
if data := ExtractPubKeyEd25519V0(pkScript); data != nil { | ||
addr, err := stdaddr.NewAddressPubKeyEd25519V0Raw(data, params) | ||
return STPubKeyEd25519, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for pay-to-pubkey-schnorr-secp256k1 script. | ||
if data := ExtractPubKeySchnorrSecp256k1V0(pkScript); data != nil { | ||
addr, err := stdaddr.NewAddressPubKeySchnorrSecp256k1V0Raw(data, params) | ||
return STPubKeySchnorrSecp256k1, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for multi-signature script. | ||
details := ExtractMultiSigScriptDetailsV0(pkScript, true) | ||
if details.Valid { | ||
// Convert the public keys while skipping any that are invalid. Also, | ||
// only allocate the slice of addresses if at least one valid address is | ||
// found to avoid an unnecessary heap alloc that would otherwise happen | ||
// when there are no valid addresses because the slice is returned. | ||
var addrs []stdaddr.Address | ||
for i := uint16(0); i < details.NumPubKeys; i++ { | ||
pubkey, err := secp256k1.ParsePubKey(details.PubKeys[i]) | ||
if err == nil { | ||
addr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0(pubkey, params) | ||
if err == nil { | ||
if addrs == nil { | ||
addrs = make([]stdaddr.Address, 0, details.NumPubKeys-i) | ||
} | ||
addrs = append(addrs, addr) | ||
} | ||
} | ||
} | ||
return STMultiSig, addrs | ||
} | ||
|
||
// Check for stake submission script. Only stake-submission-tagged | ||
// pay-to-pubkey-hash and pay-to-script-hash are allowed. | ||
if h := ExtractStakeSubmissionPubKeyHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(h, params) | ||
return STStakeSubmissionPubKeyHash, addrToSlice(addr, err) | ||
} | ||
if h := ExtractStakeSubmissionScriptHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(h, params) | ||
return STStakeSubmissionScriptHash, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for stake generation script. Only stake-generation-tagged | ||
// pay-to-pubkey-hash and pay-to-script-hash are allowed. | ||
if h := ExtractStakeGenPubKeyHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(h, params) | ||
return STStakeGenPubKeyHash, addrToSlice(addr, err) | ||
} | ||
if h := ExtractStakeGenScriptHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(h, params) | ||
return STStakeGenScriptHash, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for stake revocation script. Only stake-revocation-tagged | ||
// pay-to-pubkey-hash and pay-to-script-hash are allowed. | ||
if h := ExtractStakeRevocationPubKeyHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(h, params) | ||
return STStakeRevocationPubKeyHash, addrToSlice(addr, err) | ||
} | ||
if h := ExtractStakeRevocationScriptHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(h, params) | ||
return STStakeRevocationScriptHash, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for stake change script. Only stake-change-tagged | ||
// pay-to-pubkey-hash and pay-to-script-hash are allowed. | ||
if h := ExtractStakeChangePubKeyHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(h, params) | ||
return STStakeChangePubKeyHash, addrToSlice(addr, err) | ||
} | ||
if h := ExtractStakeChangeScriptHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(h, params) | ||
return STStakeChangeScriptHash, addrToSlice(addr, err) | ||
} | ||
|
||
// Check for null data script. | ||
if IsNullDataScriptV0(pkScript) { | ||
// Null data scripts do not have an associated address. | ||
return STNullData, nil | ||
} | ||
|
||
// Check for treasury add. | ||
if IsTreasuryAddScriptV0(pkScript) { | ||
return STTreasuryAdd, nil | ||
} | ||
|
||
// Check for treasury generation script. Only treasury-gen-tagged | ||
// pay-to-pubkey-hash and pay-to-script-hash are allowed. | ||
if h := ExtractTreasuryGenPubKeyHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(h, params) | ||
return STTreasuryGenPubKeyHash, addrToSlice(addr, err) | ||
} | ||
if h := ExtractTreasuryGenScriptHashV0(pkScript); h != nil { | ||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(h, params) | ||
return STTreasuryGenScriptHash, addrToSlice(addr, err) | ||
} | ||
|
||
// Don't attempt to extract addresses for nonstandard transactions. | ||
return STNonStandard, nil | ||
} |
Oops, something went wrong.