Add ECDSA support (#1657)

* Add ECDSA support

* Add domain separation to ECDSA sighash

* Use InfallibleWrite instead of Write

* Rename funcs

* Fix wrong use if vm.sigCache

* Add TestCalculateSignatureHashECDSA

* Add consts

* Fix comment and test name

* Move consts to the top

* Fix comment
This commit is contained in:
Ori Newman 2021-04-06 14:27:18 +03:00 committed by GitHub
parent 6dd3d4a9e7
commit a786cdc15e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 691 additions and 70 deletions

View File

@ -189,7 +189,7 @@ func (v *transactionValidator) validateTransactionScripts(tx *externalapi.Domain
}
scriptPubKey := utxoEntry.ScriptPublicKey()
vm, err := txscript.NewEngine(scriptPubKey, tx, i, txscript.ScriptNoFlags, v.sigCache, sighashReusedValues)
vm, err := txscript.NewEngine(scriptPubKey, tx, i, txscript.ScriptNoFlags, v.sigCache, v.sigCacheECDSA, sighashReusedValues)
if err != nil {
return errors.Wrapf(ruleerrors.ErrScriptMalformed, "failed to parse input "+
"%d which references output %s - "+

View File

@ -21,6 +21,7 @@ type transactionValidator struct {
massPerSigOp uint64
maxCoinbasePayloadLength uint64
sigCache *txscript.SigCache
sigCacheECDSA *txscript.SigCacheECDSA
}
// New instantiates a new TransactionValidator
@ -47,5 +48,6 @@ func New(blockCoinbaseMaturity uint64,
ghostdagDataStore: ghostdagDataStore,
daaBlocksStore: daaBlocksStore,
sigCache: txscript.NewSigCache(sigCacheSize),
sigCacheECDSA: txscript.NewSigCacheECDSA(sigCacheSize),
}
}

View File

@ -362,3 +362,127 @@ func TestSigningTwoInputs(t *testing.T) {
}
})
}
func TestSigningTwoInputsECDSA(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) {
params.BlockCoinbaseMaturity = 0
factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(params, false, "TestSigningTwoInputsECDSA")
if err != nil {
t.Fatalf("Error setting up consensus: %+v", err)
}
defer teardown(false)
privateKey, err := secp256k1.GenerateECDSAPrivateKey()
if err != nil {
t.Fatalf("Failed to generate a private key: %v", err)
}
publicKey, err := privateKey.ECDSAPublicKey()
if err != nil {
t.Fatalf("Failed to generate a public key: %v", err)
}
publicKeySerialized, err := publicKey.Serialize()
if err != nil {
t.Fatalf("Failed to serialize public key: %v", err)
}
addr, err := util.NewAddressPublicKeyECDSA(publicKeySerialized[:], params.Prefix)
if err != nil {
t.Fatalf("Failed to generate p2pk address: %v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("PayToAddrScript: unexpected error: %v", err)
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block2Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block3Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{block2Hash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block2, err := tc.GetBlock(block2Hash)
if err != nil {
t.Fatalf("Error getting block2: %+v", err)
}
block3, err := tc.GetBlock(block3Hash)
if err != nil {
t.Fatalf("Error getting block3: %+v", err)
}
block2Tx := block2.Transactions[0]
block2TxOut := block2Tx.Outputs[0]
block3Tx := block3.Transactions[0]
block3TxOut := block3Tx.Outputs[0]
tx := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: []*externalapi.DomainTransactionInput{
{
PreviousOutpoint: externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block2.Transactions[0]),
Index: 0,
},
Sequence: constants.MaxTxInSequenceNum,
UTXOEntry: utxo.NewUTXOEntry(block2TxOut.Value, block2TxOut.ScriptPublicKey, true, 0),
},
{
PreviousOutpoint: externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block3.Transactions[0]),
Index: 0,
},
Sequence: constants.MaxTxInSequenceNum,
UTXOEntry: utxo.NewUTXOEntry(block3TxOut.Value, block3TxOut.ScriptPublicKey, true, 0),
},
},
Outputs: []*externalapi.DomainTransactionOutput{{
Value: 1,
ScriptPublicKey: &externalapi.ScriptPublicKey{
Script: nil,
Version: 0,
},
}},
SubnetworkID: subnetworks.SubnetworkIDNative,
Gas: 0,
LockTime: 0,
}
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, input := range tx.Inputs {
signatureScript, err := txscript.SignatureScriptECDSA(tx, i, consensushashing.SigHashAll, privateKey,
sighashReusedValues)
if err != nil {
t.Fatalf("Failed to create a sigScript: %v", err)
}
input.SignatureScript = signatureScript
}
_, insertionResult, err := tc.AddBlock([]*externalapi.DomainHash{block3Hash}, nil, []*externalapi.DomainTransaction{tx})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
txOutpoint := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(tx),
Index: 0,
}
if !insertionResult.VirtualUTXODiff.ToAdd().Contains(txOutpoint) {
t.Fatalf("tx was not accepted by the DAG")
}
})
}

View File

@ -2,7 +2,6 @@ package consensushashing
import (
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashes"
"github.com/kaspanet/kaspad/domain/consensus/utils/serialization"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
@ -58,10 +57,10 @@ type SighashReusedValues struct {
payloadHash *externalapi.DomainHash
}
// CalculateSignatureHash will, given a script and hash type calculate the signature hash
// to be used for signing and verification.
// CalculateSignatureHashSchnorr will, given a script and hash type calculate the signature hash
// to be used for signing and verification for Schnorr.
// This returns error only if one of the provided parameters are consensus-invalid.
func CalculateSignatureHash(tx *externalapi.DomainTransaction, inputIndex int, hashType SigHashType,
func CalculateSignatureHashSchnorr(tx *externalapi.DomainTransaction, inputIndex int, hashType SigHashType,
reusedValues *SighashReusedValues) (*externalapi.DomainHash, error) {
if !hashType.IsStandardSigHashType() {
@ -70,18 +69,26 @@ func CalculateSignatureHash(tx *externalapi.DomainTransaction, inputIndex int, h
txIn := tx.Inputs[inputIndex]
prevScriptPublicKey := txIn.UTXOEntry.ScriptPublicKey()
if tx.Version > constants.MaxTransactionVersion {
return nil, errors.Errorf("Transaction version is unknown.")
}
if prevScriptPublicKey.Version > constants.MaxScriptPublicKeyVersion {
return nil, errors.Errorf("Script version is unknown.")
}
return calculateSignatureHash(tx, inputIndex, txIn, prevScriptPublicKey, hashType, reusedValues)
}
// CalculateSignatureHashECDSA will, given a script and hash type calculate the signature hash
// to be used for signing and verification for ECDSA.
// This returns error only if one of the provided parameters are consensus-invalid.
func CalculateSignatureHashECDSA(tx *externalapi.DomainTransaction, inputIndex int, hashType SigHashType,
reusedValues *SighashReusedValues) (*externalapi.DomainHash, error) {
hash, err := CalculateSignatureHashSchnorr(tx, inputIndex, hashType, reusedValues)
if err != nil {
return nil, err
}
hashWriter := hashes.NewTransactionSigningHashECDSAWriter()
hashWriter.InfallibleWrite(hash.ByteSlice())
return hashWriter.Finalize(), nil
}
func calculateSignatureHash(tx *externalapi.DomainTransaction, inputIndex int, txIn *externalapi.DomainTransactionInput,
prevScriptPublicKey *externalapi.ScriptPublicKey, hashType SigHashType, reusedValues *SighashReusedValues) (
*externalapi.DomainHash, error) {

View File

@ -88,7 +88,7 @@ func modifySubnetworkID(tx *externalapi.DomainTransaction) *externalapi.DomainTr
return clone
}
func TestCalculateSignatureHash(t *testing.T) {
func TestCalculateSignatureHashSchnorr(t *testing.T) {
nativeTx, subnetworkTx, err := generateTxs()
if err != nil {
t.Fatalf("Error from generateTxs: %+v", err)
@ -196,10 +196,132 @@ func TestCalculateSignatureHash(t *testing.T) {
tx = test.modificationFunction(tx)
}
actualSignatureHash, err := consensushashing.CalculateSignatureHash(
actualSignatureHash, err := consensushashing.CalculateSignatureHashSchnorr(
tx, test.inputIndex, test.hashType, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("%s: Error from CalculateSignatureHash: %+v", test.name, err)
t.Errorf("%s: Error from CalculateSignatureHashSchnorr: %+v", test.name, err)
continue
}
if actualSignatureHash.String() != test.expectedSignatureHash {
t.Errorf("%s: expected signature hash: '%s'; but got: '%s'",
test.name, test.expectedSignatureHash, actualSignatureHash)
}
}
}
func TestCalculateSignatureHashECDSA(t *testing.T) {
nativeTx, subnetworkTx, err := generateTxs()
if err != nil {
t.Fatalf("Error from generateTxs: %+v", err)
}
// Note: Expected values were generated by the same code that they test,
// As long as those were not verified using 3rd-party code they only check for regression, not correctness
tests := []struct {
name string
tx *externalapi.DomainTransaction
hashType consensushashing.SigHashType
inputIndex int
modificationFunction func(*externalapi.DomainTransaction) *externalapi.DomainTransaction
expectedSignatureHash string
}{
// native transactions
// sigHashAll
{name: "native-all-0", tx: nativeTx, hashType: all, inputIndex: 0,
expectedSignatureHash: "150a2bcd0296f76a3395a4a9982df46bf24ce93f955bc39c10ffc95b6c524eb3"},
{name: "native-all-0-modify-input-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifyInput(1), // should change the hash
expectedSignatureHash: "8fb5304e181b003e0c123ea6f6677abc3704feec47054e8a1c218b827bf57ca0"},
{name: "native-all-0-modify-output-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifyOutput(1), // should change the hash
expectedSignatureHash: "180cb36454aa80822694decde4fc711104e35a4bddf92286a83877f2b8d0aabb"},
{name: "native-all-0-modify-sequence-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifySequence(1), // should change the hash
expectedSignatureHash: "5b5f1c42a3c3c16bb4922777e2963c00e6a2cce39afa1980d2288053378f9632"},
{name: "native-all-anyonecanpay-0", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "9473ffbe0db4914f2cd8fe5d67479224a02eb031882d9170b785d0d2c7bfcd1b"},
{name: "native-all-anyonecanpay-0-modify-input-0", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyInput(0), // should change the hash
expectedSignatureHash: "1208491d564c138d613f51b997394dbad23feca7c0ca88c7f36cdf6b9173327d"},
{name: "native-all-anyonecanpay-0-modify-input-1", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyInput(1), // shouldn't change the hash
expectedSignatureHash: "9473ffbe0db4914f2cd8fe5d67479224a02eb031882d9170b785d0d2c7bfcd1b"},
{name: "native-all-anyonecanpay-0-modify-sequence", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "9473ffbe0db4914f2cd8fe5d67479224a02eb031882d9170b785d0d2c7bfcd1b"},
// sigHashNone
{name: "native-none-0", tx: nativeTx, hashType: none, inputIndex: 0,
expectedSignatureHash: "6e427f26e4a9c1a7fc556a8aabdedb8799a897bc5d42a0a18615e5a0f7639d8f"},
{name: "native-none-0-modify-output-1", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "6e427f26e4a9c1a7fc556a8aabdedb8799a897bc5d42a0a18615e5a0f7639d8f"},
{name: "native-none-0-modify-sequence-0", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifySequence(0), // should change the hash
expectedSignatureHash: "57d76e2568cd3fc3426b4f8836fe900a2d20e740fad744949126651fd549f75e"},
{name: "native-none-0-modify-sequence-1", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "6e427f26e4a9c1a7fc556a8aabdedb8799a897bc5d42a0a18615e5a0f7639d8f"},
{name: "native-none-anyonecanpay-0", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "ef97a0f89d623302619f9aa2a00fce1522e72d4d255e6c6d3ed225ffc02f38ff"},
{name: "native-none-anyonecanpay-0-modify-amount-spent", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyAmountSpent(0), // should change the hash
expectedSignatureHash: "043a2a943f02607be126ac6609ab2324aae389d784a4147f27101e7da379311a"},
{name: "native-none-anyonecanpay-0-modify-script-public-key", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyScriptPublicKey(0), // should change the hash
expectedSignatureHash: "f2cd43d0d047cdcfbf8b6e12a86cfbf250f1e2c34dc5e631675a5f5b867bd9e6"},
// sigHashSingle
{name: "native-single-0", tx: nativeTx, hashType: single, inputIndex: 0,
expectedSignatureHash: "1cf376b9f180f59a1b9a5e420390198c20e1ba79c39349271632145fda175247"},
{name: "native-single-0-modify-output-0", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifyOutput(0), // should change the hash
expectedSignatureHash: "c2c7e77516a15f0f47f886b14cc47af2045eea15f176a9a560a9d47d8866958f"},
{name: "native-single-0-modify-output-1", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "1cf376b9f180f59a1b9a5e420390198c20e1ba79c39349271632145fda175247"},
{name: "native-single-0-modify-sequence-0", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifySequence(0), // should change the hash
expectedSignatureHash: "2034eec2acc08c49d3896cc1bda214904ca850fc5989518885465b5a3154ee7f"},
{name: "native-single-0-modify-sequence-1", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "1cf376b9f180f59a1b9a5e420390198c20e1ba79c39349271632145fda175247"},
{name: "native-single-2-no-corresponding-output", tx: nativeTx, hashType: single, inputIndex: 2,
expectedSignatureHash: "84ae3bb03202efc587d97e5aea7b80581b82242b969e6dea13b8daa32d24c0c1"},
{name: "native-single-2-no-corresponding-output-modify-output-1", tx: nativeTx, hashType: single, inputIndex: 2,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "84ae3bb03202efc587d97e5aea7b80581b82242b969e6dea13b8daa32d24c0c1"},
{name: "native-single-anyonecanpay-0", tx: nativeTx, hashType: singleAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "b2ccf259a65c3231d741a03420967b95563c3928cc15d3d15e8e795f383ab48b"},
{name: "native-single-anyonecanpay-2-no-corresponding-output", tx: nativeTx, hashType: singleAnyoneCanPay, inputIndex: 2,
expectedSignatureHash: "652c8cd0ac050e41aad347ea09ee788360eec70908ba22fe5bba5bdde49b8ae1"},
// subnetwork transaction
{name: "subnetwork-all-0", tx: subnetworkTx, hashType: all, inputIndex: 0,
expectedSignatureHash: "2e828c04f5f03e4ce4b3de1fa5303400da5fa504291b760f5f6d4e98fc24597f"},
{name: "subnetwork-all-modify-payload", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifyPayload, // should change the hash
expectedSignatureHash: "d5f3993aa8b7f47df52f78f2be9965f928c9cca9ac9e9542f1190b9d5ed6c17d"},
{name: "subnetwork-all-modify-gas", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifyGas, // should change the hash
expectedSignatureHash: "e74d4a9fa5cdf476299ebdfa03f3c8021a157f814731ea11f6a6d606dc5cd439"},
{name: "subnetwork-all-subnetwork-id", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifySubnetworkID, // should change the hash
expectedSignatureHash: "ca8bf9bc42cda2ec3ce8bee090011072e56ff4d0d8616d5c20cefe5f84d7fb37"},
}
for _, test := range tests {
tx := test.tx
if test.modificationFunction != nil {
tx = test.modificationFunction(tx)
}
actualSignatureHash, err := consensushashing.CalculateSignatureHashECDSA(
tx, test.inputIndex, test.hashType, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("%s: Error from CalculateSignatureHashECDSA: %+v", test.name, err)
continue
}
@ -283,7 +405,7 @@ func generateTxs() (nativeTx, subnetworkTx *externalapi.DomainTransaction, err e
return nativeTx, subnetworkTx, nil
}
func BenchmarkCalculateSignatureHash(b *testing.B) {
func BenchmarkCalculateSignatureHashSchnorr(b *testing.B) {
sigHashTypes := []consensushashing.SigHashType{
consensushashing.SigHashAll,
consensushashing.SigHashNone,
@ -300,9 +422,9 @@ func BenchmarkCalculateSignatureHash(b *testing.B) {
reusedValues := &consensushashing.SighashReusedValues{}
for inputIndex := range tx.Inputs {
sigHashType := sigHashTypes[inputIndex%len(sigHashTypes)]
_, err := consensushashing.CalculateSignatureHash(tx, inputIndex, sigHashType, reusedValues)
_, err := consensushashing.CalculateSignatureHashSchnorr(tx, inputIndex, sigHashType, reusedValues)
if err != nil {
b.Fatalf("Error from CalculateSignatureHash: %+v", err)
b.Fatalf("Error from CalculateSignatureHashSchnorr: %+v", err)
}
}
}

View File

@ -22,28 +22,6 @@ const (
txEncodingExcludeSignatureScript = 1 << iota
)
// TransactionHashForSigning hashes the transaction and the given hash type in a way that is intended for
// signatures.
func TransactionHashForSigning(tx *externalapi.DomainTransaction, hashType uint32) *externalapi.DomainHash {
// Encode the header and hash everything prior to the number of
// transactions.
writer := hashes.NewTransactionSigningHashWriter()
err := serializeTransaction(writer, tx, txEncodingFull)
if err != nil {
// It seems like this could only happen if the writer returned an error.
// and this writer should never return an error (no allocations or possible failures)
// the only non-writer error path here is unknown types in `WriteElement`
panic(errors.Wrap(err, "TransactionHashForSigning() failed. this should never fail for structurally-valid transactions"))
}
err = serialization.WriteElement(writer, hashType)
if err != nil {
panic(errors.Wrap(err, "this should never happen. Hash digest should never return an error"))
}
return writer.Finalize()
}
// TransactionHash returns the transaction hash.
func TransactionHash(tx *externalapi.DomainTransaction) *externalapi.DomainHash {
// Encode the header and hash everything prior to the number of

View File

@ -1,19 +1,26 @@
package hashes
import (
"crypto/sha256"
"github.com/pkg/errors"
"golang.org/x/crypto/blake2b"
)
const (
transcationHashDomain = "TransactionHash"
transcationIDDomain = "TransactionID"
transcationSigningDomain = "TransactionSigningHash"
blockDomain = "BlockHash"
proofOfWorkDomain = "ProofOfWorkHash"
merkleBranchDomain = "MerkleBranchHash"
transcationHashDomain = "TransactionHash"
transcationIDDomain = "TransactionID"
transcationSigningDomain = "TransactionSigningHash"
transcationSigningECDSADomain = "TransactionSigningHashECDSA"
blockDomain = "BlockHash"
proofOfWorkDomain = "ProofOfWorkHash"
merkleBranchDomain = "MerkleBranchHash"
)
// transactionSigningECDSADomainHash is a hashed version of transcationSigningECDSADomain that is used
// to make it a constant size. This is needed because this domain is used by sha256 hash writer, and
// sha256 doesn't support variable size domain separation.
var transactionSigningECDSADomainHash = sha256.Sum256([]byte(transcationSigningECDSADomain))
// NewTransactionHashWriter Returns a new HashWriter used for transaction hashes
func NewTransactionHashWriter() HashWriter {
blake, err := blake2b.New256([]byte(transcationHashDomain))
@ -41,6 +48,13 @@ func NewTransactionSigningHashWriter() HashWriter {
return HashWriter{blake}
}
// NewTransactionSigningHashECDSAWriter Returns a new HashWriter used for signing on a transaction with ECDSA
func NewTransactionSigningHashECDSAWriter() HashWriter {
hashWriter := HashWriter{sha256.New()}
hashWriter.InfallibleWrite(transactionSigningECDSADomainHash[:])
return hashWriter
}
// NewBlockHashWriter Returns a new HashWriter used for hashing blocks
func NewBlockHashWriter() HashWriter {
blake, err := blake2b.New256([]byte(blockDomain))

View File

@ -53,6 +53,7 @@ type Engine struct {
numOps int
flags ScriptFlags
sigCache *SigCache
sigCacheECDSA *SigCacheECDSA
sigHashReusedValues *consensushashing.SighashReusedValues
isP2SH bool // treat execution as pay-to-script-hash
savedFirstStack [][]byte // stack from first script for ps2h scripts
@ -369,6 +370,14 @@ func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error {
return scriptError(ErrPubKeyFormat, "unsupported public key type")
}
func (vm *Engine) checkPubKeyEncodingECDSA(pubKey []byte) error {
if len(pubKey) == 33 {
return nil
}
return scriptError(ErrPubKeyFormat, "unsupported public key type")
}
// checkSignatureLength returns whether or not the passed signature is
// in the correct Schnorr format.
func (vm *Engine) checkSignatureLength(sig []byte) error {
@ -379,6 +388,14 @@ func (vm *Engine) checkSignatureLength(sig []byte) error {
return nil
}
func (vm *Engine) checkSignatureLengthECDSA(sig []byte) error {
if len(sig) != 64 {
message := fmt.Sprintf("invalid signature length %d", len(sig))
return scriptError(ErrSigLength, message)
}
return nil
}
// getStack returns the contents of stack as a byte array bottom up
func getStack(stack *stack) [][]byte {
array := make([][]byte, stack.Depth())
@ -428,7 +445,7 @@ func (vm *Engine) SetAltStack(data [][]byte) {
// transaction, and input index. The flags modify the behavior of the script
// engine according to the description provided by each flag.
func NewEngine(scriptPubKey *externalapi.ScriptPublicKey, tx *externalapi.DomainTransaction, txIdx int, flags ScriptFlags,
sigCache *SigCache, sighashReusedValues *consensushashing.SighashReusedValues) (*Engine, error) {
sigCache *SigCache, sigCacheECDSA *SigCacheECDSA, sighashReusedValues *consensushashing.SighashReusedValues) (*Engine, error) {
// The provided transaction input index must refer to a valid input.
if txIdx < 0 || txIdx >= len(tx.Inputs) {
@ -446,7 +463,7 @@ func NewEngine(scriptPubKey *externalapi.ScriptPublicKey, tx *externalapi.Domain
return nil, scriptError(ErrEvalFalse,
"false stack entry at end of script execution")
}
vm := Engine{scriptVersion: scriptPubKey.Version, flags: flags, sigCache: sigCache}
vm := Engine{scriptVersion: scriptPubKey.Version, flags: flags, sigCache: sigCache, sigCacheECDSA: sigCacheECDSA}
if vm.scriptVersion > constants.MaxScriptPublicKeyVersion {
return &vm, nil

View File

@ -56,7 +56,7 @@ func TestBadPC(t *testing.T) {
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm("NOP", 0), Version: 0}
for _, test := range tests {
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("Failed to create script: %v", err)
}
@ -124,7 +124,7 @@ func TestCheckErrorCondition(t *testing.T) {
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm(test.script, 0), Version: 0}
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("TestCheckErrorCondition: %d: failed to create script: %v", i, err)
}
@ -252,7 +252,7 @@ func TestDisasmPC(t *testing.T) {
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm("OP_DROP NOP TRUE", 0), Version: 0}
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Fatalf("failed to create script: %v", err)
}
@ -315,7 +315,7 @@ func TestDisasmScript(t *testing.T) {
}
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm("OP_DROP NOP TRUE", 0), Version: 0}
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Fatalf("failed to create script: %v", err)
}

View File

@ -206,7 +206,7 @@ const (
OpSHA256 = 0xa8 // 168
OpUnknown169 = 0xa9 // 169
OpBlake2b = 0xaa // 170
OpUnknown171 = 0xab // 171
OpCheckSigECDSA = 0xab // 171
OpCheckSig = 0xac // 172
OpCheckSigVerify = 0xad // 173
OpCheckMultiSig = 0xae // 174
@ -487,6 +487,7 @@ var opcodeArray = [256]opcode{
// Crypto opcodes.
OpSHA256: {OpSHA256, "OP_SHA256", 1, opcodeSha256},
OpBlake2b: {OpBlake2b, "OP_BLAKE2B", 1, opcodeBlake2b},
OpCheckSigECDSA: {OpCheckSigECDSA, "OP_CHECKSIGECDSA", 1, opcodeCheckSigECDSA},
OpCheckSig: {OpCheckSig, "OP_CHECKSIG", 1, opcodeCheckSig},
OpCheckSigVerify: {OpCheckSigVerify, "OP_CHECKSIGVERIFY", 1, opcodeCheckSigVerify},
OpCheckMultiSig: {OpCheckMultiSig, "OP_CHECKMULTISIG", 1, opcodeCheckMultiSig},
@ -508,7 +509,6 @@ var opcodeArray = [256]opcode{
OpUnknown166: {OpUnknown166, "OP_UNKNOWN166", 1, opcodeInvalid},
OpUnknown167: {OpUnknown167, "OP_UNKNOWN167", 1, opcodeInvalid},
OpUnknown169: {OpUnknown169, "OP_UNKNOWN169", 1, opcodeInvalid},
OpUnknown171: {OpUnknown171, "OP_UNKNOWN171", 1, opcodeInvalid},
OpUnknown188: {OpUnknown188, "OP_UNKNOWN188", 1, opcodeInvalid},
OpUnknown189: {OpUnknown189, "OP_UNKNOWN189", 1, opcodeInvalid},
OpUnknown190: {OpUnknown190, "OP_UNKNOWN190", 1, opcodeInvalid},
@ -1988,7 +1988,7 @@ func opcodeCheckSig(op *parsedOpcode, vm *Engine) error {
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHash(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
sigHash, err := consensushashing.CalculateSignatureHashSchnorr(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
vm.dstack.PushBool(false)
return nil
@ -2027,6 +2027,89 @@ func opcodeCheckSig(op *parsedOpcode, vm *Engine) error {
return nil
}
func opcodeCheckSigECDSA(op *parsedOpcode, vm *Engine) error {
pkBytes, err := vm.dstack.PopByteArray()
if err != nil {
return err
}
fullSigBytes, err := vm.dstack.PopByteArray()
if err != nil {
return err
}
// The signature actually needs needs to be longer than this, but at
// least 1 byte is needed for the hash type below. The full length is
// checked depending on the script flags and upon parsing the signature.
if len(fullSigBytes) < 1 {
vm.dstack.PushBool(false)
return nil
}
// Trim off hashtype from the signature string and check if the
// signature and pubkey conform to the strict encoding requirements
// depending on the flags.
//
// NOTE: When the strict encoding flags are set, any errors in the
// signature or public encoding here result in an immediate script error
// (and thus no result bool is pushed to the data stack). This differs
// from the logic below where any errors in parsing the signature is
// treated as the signature failure resulting in false being pushed to
// the data stack. This is required because the more general script
// validation consensus rules do not have the new strict encoding
// requirements enabled by the flags.
hashType := consensushashing.SigHashType(fullSigBytes[len(fullSigBytes)-1])
sigBytes := fullSigBytes[:len(fullSigBytes)-1]
if !hashType.IsStandardSigHashType() {
return scriptError(ErrInvalidSigHashType, fmt.Sprintf("invalid hash type 0x%x", hashType))
}
if err := vm.checkSignatureLengthECDSA(sigBytes); err != nil {
return err
}
if err := vm.checkPubKeyEncodingECDSA(pkBytes); err != nil {
return err
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHashECDSA(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
vm.dstack.PushBool(false)
return nil
}
pubKey, err := secp256k1.DeserializeECDSAPubKey(pkBytes)
if err != nil {
vm.dstack.PushBool(false)
return nil
}
signature, err := secp256k1.DeserializeECDSASignatureFromSlice(sigBytes)
if err != nil {
vm.dstack.PushBool(false)
return nil
}
var valid bool
secpHash := secp256k1.Hash(*sigHash.ByteArray())
if vm.sigCacheECDSA != nil {
valid = vm.sigCacheECDSA.Exists(secpHash, signature, pubKey)
if !valid && pubKey.ECDSAVerify(&secpHash, signature) {
vm.sigCacheECDSA.Add(secpHash, signature, pubKey)
valid = true
}
} else {
valid = pubKey.ECDSAVerify(&secpHash, signature)
}
if !valid && len(sigBytes) > 0 {
str := "signature not empty on failed checksig"
return scriptError(ErrNullFail, str)
}
vm.dstack.PushBool(valid)
return nil
}
// opcodeCheckSigVerify is a combination of opcodeCheckSig and opcodeVerify.
// The opcodeCheckSig function is invoked followed by opcodeVerify. See the
// documentation for each of those opcodes for more details.
@ -2194,7 +2277,7 @@ func opcodeCheckMultiSig(op *parsedOpcode, vm *Engine) error {
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHash(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
sigHash, err := consensushashing.CalculateSignatureHashSchnorr(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
return err
}

View File

@ -73,7 +73,7 @@ func TestOpcodeDisasm(t *testing.T) {
0xa3: "OP_MIN", 0xa4: "OP_MAX", 0xa5: "OP_WITHIN",
0xa8: "OP_SHA256",
0xaa: "OP_BLAKE2B",
0xac: "OP_CHECKSIG", 0xad: "OP_CHECKSIGVERIFY",
0xab: "OP_CHECKSIGECDSA", 0xac: "OP_CHECKSIG", 0xad: "OP_CHECKSIGVERIFY",
0xae: "OP_CHECKMULTISIG", 0xaf: "OP_CHECKMULTISIGVERIFY",
0xb0: "OP_CHECKLOCKTIMEVERIFY", 0xb1: "OP_CHECKSEQUENCEVERIFY",
0xfa: "OP_SMALLINTEGER", 0xfb: "OP_PUBKEYS",
@ -186,7 +186,7 @@ func TestOpcodeDisasm(t *testing.T) {
}
func isOpUnknown(opcodeVal int) bool {
return opcodeVal >= 0xba && opcodeVal <= 0xf9 || opcodeVal == 0xfc || opcodeVal == 0xab ||
return opcodeVal >= 0xba && opcodeVal <= 0xf9 || opcodeVal == 0xfc ||
opcodeVal == 0xa6 || opcodeVal == 0xa7 || opcodeVal == 0xa9
}

View File

@ -260,8 +260,10 @@ func createSpendingTx(sigScript []byte, scriptPubKey *externalapi.ScriptPublicKe
func testScripts(t *testing.T, tests [][]interface{}, useSigCache bool) {
// Create a signature cache to use only if requested.
var sigCache *SigCache
var sigCacheECDSA *SigCacheECDSA
if useSigCache {
sigCache = NewSigCache(10)
sigCacheECDSA = NewSigCacheECDSA(10)
}
for i, test := range tests {
@ -343,7 +345,7 @@ func testScripts(t *testing.T, tests [][]interface{}, useSigCache bool) {
// used, then create a new engine to execute the scripts.
tx := createSpendingTx(scriptSig, scriptPubKey)
vm, err := NewEngine(scriptPubKey, tx, 0, flags, sigCache, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, flags, sigCache, sigCacheECDSA, &consensushashing.SighashReusedValues{})
if err == nil {
err = vm.Execute()
}

View File

@ -19,7 +19,7 @@ type sigCacheEntry struct {
pubKey *secp256k1.SchnorrPublicKey
}
// SigCache implements an ECDSA signature verification cache with a randomized
// SigCache implements an Schnorr signature verification cache with a randomized
// entry eviction policy. Only valid signatures will be added to the cache. The
// benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a DoS
// attack wherein an attack causes a victim's client to hang due to worst-case

View File

@ -0,0 +1,90 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package txscript
import (
"github.com/kaspanet/go-secp256k1"
)
// sigCacheEntryECDSA represents an entry in the SigCache. Entries within the
// SigCache are keyed according to the sigHash of the signature. In the
// scenario of a cache-hit (according to the sigHash), an additional comparison
// of the signature, and public key will be executed in order to ensure a complete
// match. In the occasion that two sigHashes collide, the newer sigHash will
// simply overwrite the existing entry.
type sigCacheEntryECDSA struct {
sig *secp256k1.ECDSASignature
pubKey *secp256k1.ECDSAPublicKey
}
// SigCacheECDSA implements an ECDSA signature verification cache with a randomized
// entry eviction policy. Only valid signatures will be added to the cache. The
// benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a DoS
// attack wherein an attack causes a victim's client to hang due to worst-case
// behavior triggered while processing attacker crafted invalid transactions. A
// detailed description of the mitigated DoS attack can be found here:
// https://bitslog.wordpress.com/2013/01/23/fixed-bitcoin-vulnerability-explanation-why-the-signature-cache-is-a-dos-protection/.
// Secondly, usage of the SigCache introduces a signature verification
// optimization which speeds up the validation of transactions within a block,
// if they've already been seen and verified within the mempool.
type SigCacheECDSA struct {
validSigs map[secp256k1.Hash]sigCacheEntryECDSA
maxEntries uint
}
// NewSigCacheECDSA creates and initializes a new instance of SigCache. Its sole
// parameter 'maxEntries' represents the maximum number of entries allowed to
// exist in the SigCache at any particular moment. Random entries are evicted
// to make room for new entries that would cause the number of entries in the
// cache to exceed the max.
func NewSigCacheECDSA(maxEntries uint) *SigCacheECDSA {
return &SigCacheECDSA{
validSigs: make(map[secp256k1.Hash]sigCacheEntryECDSA, maxEntries),
maxEntries: maxEntries,
}
}
// Exists returns true if an existing entry of 'sig' over 'sigHash' for public
// key 'pubKey' is found within the SigCache. Otherwise, false is returned.
//
// NOTE: This function is safe for concurrent access. Readers won't be blocked
// unless there exists a writer, adding an entry to the SigCache.
func (s *SigCacheECDSA) Exists(sigHash secp256k1.Hash, sig *secp256k1.ECDSASignature, pubKey *secp256k1.ECDSAPublicKey) bool {
entry, ok := s.validSigs[sigHash]
return ok && entry.pubKey.IsEqual(pubKey) && entry.sig.IsEqual(sig)
}
// Add adds an entry for a signature over 'sigHash' under public key 'pubKey'
// to the signature cache. In the event that the SigCache is 'full', an
// existing entry is randomly chosen to be evicted in order to make space for
// the new entry.
//
// NOTE: This function is safe for concurrent access. Writers will block
// simultaneous readers until function execution has concluded.
func (s *SigCacheECDSA) Add(sigHash secp256k1.Hash, sig *secp256k1.ECDSASignature, pubKey *secp256k1.ECDSAPublicKey) {
if s.maxEntries == 0 {
return
}
// If adding this new entry will put us over the max number of allowed
// entries, then evict an entry.
if uint(len(s.validSigs)+1) > s.maxEntries {
// Remove a random entry from the map. Relying on the random
// starting point of Go's map iteration. It's worth noting that
// the random iteration starting point is not 100% guaranteed
// by the spec, however most Go compilers support it.
// Ultimately, the iteration order isn't important here because
// in order to manipulate which items are evicted, an adversary
// would need to be able to execute preimage attacks on the
// hashing function in order to start eviction at a specific
// entry.
for sigEntry := range s.validSigs {
delete(s.validSigs, sigEntry)
break
}
}
s.validSigs[sigHash] = sigCacheEntryECDSA{sig, pubKey}
}

View File

@ -19,7 +19,7 @@ import (
func RawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
key *secp256k1.SchnorrKeyPair, sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
hash, err := consensushashing.CalculateSignatureHash(tx, idx, hashType, sighashReusedValues)
hash, err := consensushashing.CalculateSignatureHashSchnorr(tx, idx, hashType, sighashReusedValues)
if err != nil {
return nil, err
}
@ -32,8 +32,26 @@ func RawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType conse
return append(signature.Serialize()[:], byte(hashType)), nil
}
// RawTxInSignatureECDSA returns the serialized ECDSA signature for the input idx of
// the given transaction, with hashType appended to it.
func RawTxInSignatureECDSA(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
key *secp256k1.ECDSAPrivateKey, sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
hash, err := consensushashing.CalculateSignatureHashECDSA(tx, idx, hashType, sighashReusedValues)
if err != nil {
return nil, err
}
secpHash := secp256k1.Hash(*hash.ByteArray())
signature, err := key.ECDSASign(&secpHash)
if err != nil {
return nil, errors.Errorf("cannot sign tx input: %s", err)
}
return append(signature.Serialize()[:], byte(hashType)), nil
}
// SignatureScript creates an input signature script for tx to spend KAS sent
// from a previous output to the owner of privKey. tx must include all
// from a previous output to the owner of a Schnorr private key. tx must include all
// transaction inputs and outputs, however txin scripts are allowed to be filled
// or empty. The returned script is calculated to be used as the idx'th txin
// sigscript for tx. script is the ScriptPublicKey of the previous output being used
@ -51,6 +69,25 @@ func SignatureScript(tx *externalapi.DomainTransaction, idx int, hashType consen
return NewScriptBuilder().AddData(sig).Script()
}
// SignatureScriptECDSA creates an input signature script for tx to spend KAS sent
// from a previous output to the owner of an ECDSA private key. tx must include all
// transaction inputs and outputs, however txin scripts are allowed to be filled
// or empty. The returned script is calculated to be used as the idx'th txin
// sigscript for tx. script is the ScriptPublicKey of the previous output being used
// as the idx'th input. privKey is serialized in either a compressed or
// uncompressed format based on compress. This format must match the same format
// used to generate the payment address, or the script validation will fail.
func SignatureScriptECDSA(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
privKey *secp256k1.ECDSAPrivateKey, sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
sig, err := RawTxInSignatureECDSA(tx, idx, hashType, privKey, sighashReusedValues)
if err != nil {
return nil, err
}
return NewScriptBuilder().AddData(sig).Script()
}
func sign(dagParams *dagconfig.Params, tx *externalapi.DomainTransaction, idx int,
script *externalapi.ScriptPublicKey, hashType consensushashing.SigHashType,
sighashReusedValues *consensushashing.SighashReusedValues, kdb KeyDB, sdb ScriptDB) (

View File

@ -54,7 +54,7 @@ func checkScripts(msg string, tx *externalapi.DomainTransaction, idx int, sigScr
tx.Inputs[idx].SignatureScript = sigScript
var flags ScriptFlags
vm, err := NewEngine(scriptPubKey, tx, idx,
flags, nil, &consensushashing.SighashReusedValues{})
flags, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
return errors.Errorf("failed to make script engine for %s: %v",
msg, err)
@ -870,7 +870,7 @@ nexttest:
// Validate tx input scripts
var scriptFlags ScriptFlags
for j := range tx.Inputs {
vm, err := NewEngine(sigScriptTests[i].inputs[j].txout.ScriptPublicKey, tx, j, scriptFlags, nil,
vm, err := NewEngine(sigScriptTests[i].inputs[j].txout.ScriptPublicKey, tx, j, scriptFlags, nil, nil,
&consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("cannot create script vm for test %v: %v",

View File

@ -25,6 +25,13 @@ const (
ScriptHashTy // Pay to script hash.
)
// Script public key versions for address types.
const (
addressPublicKeyScriptPublicKeyVersion = 0
addressPublicKeyECDSAScriptPublicKeyVersion = 0
addressScriptHashScriptPublicKeyVersion = 0
)
// scriptClassToName houses the human-readable strings which describe each
// script class.
var scriptClassToName = []string{
@ -167,8 +174,7 @@ func CalcScriptInfo(sigScript, scriptPubKey []byte, isP2SH bool) (*ScriptInfo, e
}
// payToPubKeyScript creates a new script to pay a transaction
// output to a 32-byte pubkey. It is expected that the input is a valid
// hash.
// output to a 32-byte pubkey.
func payToPubKeyScript(pubKey []byte) ([]byte, error) {
return NewScriptBuilder().
AddData(pubKey).
@ -176,6 +182,15 @@ func payToPubKeyScript(pubKey []byte) ([]byte, error) {
Script()
}
// payToPubKeyScript creates a new script to pay a transaction
// output to a 33-byte pubkey.
func payToPubKeyScriptECDSA(pubKey []byte) ([]byte, error) {
return NewScriptBuilder().
AddData(pubKey).
AddOp(OpCheckSigECDSA).
Script()
}
// payToScriptHashScript creates a new script to pay a transaction output to a
// script hash. It is expected that the input is a valid hash.
func payToScriptHashScript(scriptHash []byte) ([]byte, error) {
@ -197,7 +212,20 @@ func PayToAddrScript(addr util.Address) (*externalapi.ScriptPublicKey, error) {
if err != nil {
return nil, err
}
return &externalapi.ScriptPublicKey{script, constants.MaxScriptPublicKeyVersion}, err
return &externalapi.ScriptPublicKey{script, addressPublicKeyScriptPublicKeyVersion}, err
case *util.AddressPublicKeyECDSA:
if addr == nil {
return nil, scriptError(ErrUnsupportedAddress,
nilAddrErrStr)
}
script, err := payToPubKeyScriptECDSA(addr.ScriptAddress())
if err != nil {
return nil, err
}
return &externalapi.ScriptPublicKey{script, addressPublicKeyECDSAScriptPublicKeyVersion}, err
case *util.AddressScriptHash:
if addr == nil {
@ -208,7 +236,8 @@ func PayToAddrScript(addr util.Address) (*externalapi.ScriptPublicKey, error) {
if err != nil {
return nil, err
}
return &externalapi.ScriptPublicKey{script, constants.MaxScriptPublicKeyVersion}, err
return &externalapi.ScriptPublicKey{script, addressScriptHashScriptPublicKeyVersion}, err
}
str := fmt.Sprintf("unable to generate payment script for unsupported "+

View File

@ -23,6 +23,9 @@ const (
// PubKey addresses always have the version byte set to 0.
pubKeyAddrID = 0x00
// PubKey addresses always have the version byte set to 1.
pubKeyECDSAAddrID = 0x01
// ScriptHash addresses always have the version byte set to 8.
scriptHashAddrID = 0x08
)
@ -142,6 +145,8 @@ func DecodeAddress(addr string, expectedPrefix Bech32Prefix) (Address, error) {
switch version {
case pubKeyAddrID:
return newAddressPubKey(prefix, decoded)
case pubKeyECDSAAddrID:
return newAddressPubKeyECDSA(prefix, decoded)
case scriptHashAddrID:
return newAddressScriptHashFromHash(prefix, decoded)
default:
@ -211,6 +216,68 @@ func (a *AddressPublicKey) String() string {
return a.EncodeAddress()
}
// PublicKeySizeECDSA is the public key size for an ECDSA public key
const PublicKeySizeECDSA = 33
// AddressPublicKeyECDSA is an Address for a pay-to-pubkey (P2PK)
// ECDSA transaction.
type AddressPublicKeyECDSA struct {
prefix Bech32Prefix
publicKey [PublicKeySizeECDSA]byte
}
// NewAddressPublicKeyECDSA returns a new AddressPublicKeyECDSA. publicKey must be 33
// bytes.
func NewAddressPublicKeyECDSA(publicKey []byte, prefix Bech32Prefix) (*AddressPublicKeyECDSA, error) {
return newAddressPubKeyECDSA(prefix, publicKey)
}
// newAddressPubKeyECDSA is the internal API to create an ECDSA pubkey address
// with a known leading identifier byte for a network, rather than looking
// it up through its parameters. This is useful when creating a new address
// structure from a string encoding where the identifier byte is already
// known.
func newAddressPubKeyECDSA(prefix Bech32Prefix, publicKey []byte) (*AddressPublicKeyECDSA, error) {
// Check for a valid pubkey length.
if len(publicKey) != PublicKeySizeECDSA {
return nil, errors.Errorf("publicKey must be %d bytes", PublicKeySizeECDSA)
}
addr := &AddressPublicKeyECDSA{prefix: prefix}
copy(addr.publicKey[:], publicKey)
return addr, nil
}
// EncodeAddress returns the string encoding of a pay-to-pubkey
// address. Part of the Address interface.
func (a *AddressPublicKeyECDSA) EncodeAddress() string {
return encodeAddress(a.prefix, a.publicKey[:], pubKeyECDSAAddrID)
}
// ScriptAddress returns the bytes to be included in a txout script to pay
// to a pubkey. Part of the Address interface.
func (a *AddressPublicKeyECDSA) ScriptAddress() []byte {
return a.publicKey[:]
}
// IsForPrefix returns whether or not the pay-to-pubkey address is associated
// with the passed kaspa network.
func (a *AddressPublicKeyECDSA) IsForPrefix(prefix Bech32Prefix) bool {
return a.prefix == prefix
}
// Prefix returns the prefix for this address
func (a *AddressPublicKeyECDSA) Prefix() Bech32Prefix {
return a.prefix
}
// String returns a human-readable string for the pay-to-pubkey address.
// This is equivalent to calling EncodeAddress, but is provided so the type can
// be used as a fmt.Stringer.
func (a *AddressPublicKeyECDSA) String() string {
return a.EncodeAddress()
}
// AddressScriptHash is an Address for a pay-to-script-publicKey (P2SH)
// transaction.
type AddressScriptHash struct {

View File

@ -102,6 +102,32 @@ func TestAddresses(t *testing.T) {
expectedPrefix: util.Bech32PrefixKaspaTest,
},
// ECDSA P2PK tests.
{
name: "mainnet ecdsa p2pk",
addr: "kaspa:q835ennsep3hxfe7lnz5ee7j5jgmkjswsn35ennsep3hxfe7ln35e2sm7yrlr4w",
encoded: "kaspa:q835ennsep3hxfe7lnz5ee7j5jgmkjswsn35ennsep3hxfe7ln35e2sm7yrlr4w",
valid: true,
result: util.TstAddressPubKeyECDSA(
util.Bech32PrefixKaspa,
[util.PublicKeySizeECDSA]byte{
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c, 0xaa,
}),
f: func() (util.Address, error) {
publicKey := []byte{
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c, 0xaa}
return util.NewAddressPublicKeyECDSA(publicKey, util.Bech32PrefixKaspa)
},
passedPrefix: util.Bech32PrefixUnknown,
expectedPrefix: util.Bech32PrefixKaspa,
},
// Negative P2PK tests.
{
name: "p2pk wrong public key length",
@ -270,6 +296,9 @@ func TestAddresses(t *testing.T) {
case *util.AddressPublicKey:
saddr = util.TstAddressSAddrP2PK(encoded)
case *util.AddressPublicKeyECDSA:
saddr = util.TstAddressSAddrP2PKECDSA(encoded)
case *util.AddressScriptHash:
saddr = util.TstAddressSAddrP2SH(encoded)
}
@ -337,6 +366,12 @@ func TestAddresses(t *testing.T) {
test.name)
return
}
if !reflect.DeepEqual(addr, decoded) {
t.Errorf("%v: created address does not match the decoded address",
test.name)
return
}
}
}

View File

@ -29,6 +29,13 @@ func TstAddressPubKey(prefix Bech32Prefix, hash [PublicKeySize]byte) *AddressPub
}
}
func TstAddressPubKeyECDSA(prefix Bech32Prefix, hash [PublicKeySizeECDSA]byte) *AddressPublicKeyECDSA {
return &AddressPublicKeyECDSA{
prefix: prefix,
publicKey: hash,
}
}
// TstAddressScriptHash makes an AddressScriptHash, setting the
// unexported fields with the parameters hash and netID.
func TstAddressScriptHash(prefix Bech32Prefix, hash [blake2b.Size256]byte) *AddressScriptHash {
@ -46,6 +53,13 @@ func TstAddressSAddrP2PK(addr string) []byte {
return decoded[:PublicKeySize]
}
// TstAddressSAddr returns the expected script address bytes for
// ECDSA P2PK kaspa addresses.
func TstAddressSAddrP2PKECDSA(addr string) []byte {
_, decoded, _, _ := bech32.Decode(addr)
return decoded[:PublicKeySizeECDSA]
}
// TstAddressSAddrP2SH returns the expected script address bytes for
// P2SH kaspa addresses.
func TstAddressSAddrP2SH(addr string) []byte {