From a786cdc15efc2193e7e48be5997ea094cce51cc8 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 6 Apr 2021 14:27:18 +0300 Subject: [PATCH] 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 --- .../transaction_in_context.go | 2 +- .../transactionvalidator.go | 2 + .../transactionvalidator_test.go | 124 ++++++++++++++++ .../calculate_signature_hash.go | 33 +++-- .../calculate_signature_hash_test.go | 134 +++++++++++++++++- .../utils/consensushashing/transaction.go | 22 --- domain/consensus/utils/hashes/domains.go | 26 +++- domain/consensus/utils/txscript/engine.go | 21 ++- .../consensus/utils/txscript/engine_test.go | 8 +- domain/consensus/utils/txscript/opcode.go | 91 +++++++++++- .../consensus/utils/txscript/opcode_test.go | 4 +- .../utils/txscript/reference_test.go | 4 +- domain/consensus/utils/txscript/sigcache.go | 2 +- .../utils/txscript/sigcache_ecdsa.go | 90 ++++++++++++ domain/consensus/utils/txscript/sign.go | 41 +++++- domain/consensus/utils/txscript/sign_test.go | 4 +- domain/consensus/utils/txscript/standard.go | 37 ++++- util/address.go | 67 +++++++++ util/address_test.go | 35 +++++ util/internal_test.go | 14 ++ 20 files changed, 691 insertions(+), 70 deletions(-) create mode 100644 domain/consensus/utils/txscript/sigcache_ecdsa.go diff --git a/domain/consensus/processes/transactionvalidator/transaction_in_context.go b/domain/consensus/processes/transactionvalidator/transaction_in_context.go index 0f2e84f6b..c4e4f9db7 100644 --- a/domain/consensus/processes/transactionvalidator/transaction_in_context.go +++ b/domain/consensus/processes/transactionvalidator/transaction_in_context.go @@ -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 - "+ diff --git a/domain/consensus/processes/transactionvalidator/transactionvalidator.go b/domain/consensus/processes/transactionvalidator/transactionvalidator.go index 28ec28490..19f73188f 100644 --- a/domain/consensus/processes/transactionvalidator/transactionvalidator.go +++ b/domain/consensus/processes/transactionvalidator/transactionvalidator.go @@ -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), } } diff --git a/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go index f913826fd..89ed77b9f 100644 --- a/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go +++ b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go @@ -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") + } + }) +} diff --git a/domain/consensus/utils/consensushashing/calculate_signature_hash.go b/domain/consensus/utils/consensushashing/calculate_signature_hash.go index 5cf9db8d2..9be79e73d 100644 --- a/domain/consensus/utils/consensushashing/calculate_signature_hash.go +++ b/domain/consensus/utils/consensushashing/calculate_signature_hash.go @@ -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) { diff --git a/domain/consensus/utils/consensushashing/calculate_signature_hash_test.go b/domain/consensus/utils/consensushashing/calculate_signature_hash_test.go index 4e92ccc35..059f3bbbb 100644 --- a/domain/consensus/utils/consensushashing/calculate_signature_hash_test.go +++ b/domain/consensus/utils/consensushashing/calculate_signature_hash_test.go @@ -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) } } } diff --git a/domain/consensus/utils/consensushashing/transaction.go b/domain/consensus/utils/consensushashing/transaction.go index 21cd627ed..d59f30da0 100644 --- a/domain/consensus/utils/consensushashing/transaction.go +++ b/domain/consensus/utils/consensushashing/transaction.go @@ -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 diff --git a/domain/consensus/utils/hashes/domains.go b/domain/consensus/utils/hashes/domains.go index 9ed09d81d..67f48e507 100644 --- a/domain/consensus/utils/hashes/domains.go +++ b/domain/consensus/utils/hashes/domains.go @@ -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)) diff --git a/domain/consensus/utils/txscript/engine.go b/domain/consensus/utils/txscript/engine.go index dd41434f8..322596658 100644 --- a/domain/consensus/utils/txscript/engine.go +++ b/domain/consensus/utils/txscript/engine.go @@ -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 diff --git a/domain/consensus/utils/txscript/engine_test.go b/domain/consensus/utils/txscript/engine_test.go index e6dc6c99a..1f5582b55 100644 --- a/domain/consensus/utils/txscript/engine_test.go +++ b/domain/consensus/utils/txscript/engine_test.go @@ -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) } diff --git a/domain/consensus/utils/txscript/opcode.go b/domain/consensus/utils/txscript/opcode.go index a15e4012d..ad46e4e49 100644 --- a/domain/consensus/utils/txscript/opcode.go +++ b/domain/consensus/utils/txscript/opcode.go @@ -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 } diff --git a/domain/consensus/utils/txscript/opcode_test.go b/domain/consensus/utils/txscript/opcode_test.go index 72bd844ce..582bb3fdd 100644 --- a/domain/consensus/utils/txscript/opcode_test.go +++ b/domain/consensus/utils/txscript/opcode_test.go @@ -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 } diff --git a/domain/consensus/utils/txscript/reference_test.go b/domain/consensus/utils/txscript/reference_test.go index 100f692a4..a62ff6b38 100644 --- a/domain/consensus/utils/txscript/reference_test.go +++ b/domain/consensus/utils/txscript/reference_test.go @@ -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() } diff --git a/domain/consensus/utils/txscript/sigcache.go b/domain/consensus/utils/txscript/sigcache.go index cb6ef804f..2adee4f10 100644 --- a/domain/consensus/utils/txscript/sigcache.go +++ b/domain/consensus/utils/txscript/sigcache.go @@ -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 diff --git a/domain/consensus/utils/txscript/sigcache_ecdsa.go b/domain/consensus/utils/txscript/sigcache_ecdsa.go new file mode 100644 index 000000000..27e1ae706 --- /dev/null +++ b/domain/consensus/utils/txscript/sigcache_ecdsa.go @@ -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} +} diff --git a/domain/consensus/utils/txscript/sign.go b/domain/consensus/utils/txscript/sign.go index 96d3f31ee..589c1682f 100644 --- a/domain/consensus/utils/txscript/sign.go +++ b/domain/consensus/utils/txscript/sign.go @@ -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) ( diff --git a/domain/consensus/utils/txscript/sign_test.go b/domain/consensus/utils/txscript/sign_test.go index 18f0ea5cd..b9eb62feb 100644 --- a/domain/consensus/utils/txscript/sign_test.go +++ b/domain/consensus/utils/txscript/sign_test.go @@ -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", diff --git a/domain/consensus/utils/txscript/standard.go b/domain/consensus/utils/txscript/standard.go index b0515c546..ad358872c 100644 --- a/domain/consensus/utils/txscript/standard.go +++ b/domain/consensus/utils/txscript/standard.go @@ -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 "+ diff --git a/util/address.go b/util/address.go index 638a3a428..3b4e8f2d7 100644 --- a/util/address.go +++ b/util/address.go @@ -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 { diff --git a/util/address_test.go b/util/address_test.go index a71325f6f..5f9232b99 100644 --- a/util/address_test.go +++ b/util/address_test.go @@ -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 + } } } diff --git a/util/internal_test.go b/util/internal_test.go index 05d23552d..89dec921a 100644 --- a/util/internal_test.go +++ b/util/internal_test.go @@ -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 {