Skip to content

Commit

Permalink
stdscript: Add script to address conversion.
Browse files Browse the repository at this point in the history
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
davecgh committed Nov 15, 2021
1 parent bd52804 commit f9b2037
Show file tree
Hide file tree
Showing 4 changed files with 1,295 additions and 0 deletions.
26 changes: 26 additions & 0 deletions internal/staging/stdscript/address.go
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
}
98 changes: 98 additions & 0 deletions internal/staging/stdscript/address_test.go
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
}
}
}
169 changes: 169 additions & 0 deletions internal/staging/stdscript/addressv0.go
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
}
Loading

0 comments on commit f9b2037

Please sign in to comment.