From a7bb1853f9cf12c25a233439fc82d9f952e28cb5 Mon Sep 17 00:00:00 2001 From: talelbaz <63008512+talelbaz@users.noreply.github.com> Date: Sun, 21 Feb 2021 17:46:22 +0200 Subject: [PATCH] Adds tests for transaction validator and block validators (#1531) * [NOD-1453] cover failing block validation * [NOD-1453] Complete covering test for invalid block * [NOD-1453] Fix validator tests after rebase * [NOD-1453] Cover tests for valid blocks * [NOD-1453] Implement unit tests for ValidateTransactionInIsolation * [NOD-1453] Add tests for ValidateTransactionInContextAndPopulateMassAndFee * [NOD-1453] Cover ValidateHeaderInContext test * [NOD-1453] Fix after rebase * not finish * commited for update the branch. * Adds new tests to block_body_in_isolation_test.go according to (and instead of ) blockvalisator_test.go * Adds a comment to type MEDIAN. * Fixes according to the review notes: add notes and change variables name. * Fix comment. * Remove an unused test( all the tests in this file were passed to other test files). * Change a variable name(txWithAnEmptyInvalidScript to txWithInvalidSignature). * adds missing '}'. * Change spaces to tab Co-authored-by: karim1king Co-authored-by: Karim A Co-authored-by: tal --- domain/consensus/constructors.go | 4 + domain/consensus/factory.go | 23 +- .../block_body_in_isolation_test.go | 326 +++++++++++++++++- .../transactionvalidator_test.go | 251 ++++++++++++++ 4 files changed, 589 insertions(+), 15 deletions(-) create mode 100644 domain/consensus/processes/transactionvalidator/transactionvalidator_test.go diff --git a/domain/consensus/constructors.go b/domain/consensus/constructors.go index 81cab3905..38b91ed89 100644 --- a/domain/consensus/constructors.go +++ b/domain/consensus/constructors.go @@ -15,3 +15,7 @@ type GHOSTDAGManagerConstructor func(model.DBReader, model.DAGTopologyManager, type DifficultyManagerConstructor func(model.DBReader, model.GHOSTDAGManager, model.GHOSTDAGDataStore, model.BlockHeaderStore, model.DAGTopologyManager, model.DAGTraversalManager, *big.Int, int, bool, time.Duration, *externalapi.DomainHash) model.DifficultyManager + +// PastMedianTimeManagerConstructor is the function signature for a constructor of a type implementing model.PastMedianTimeManager +type PastMedianTimeManagerConstructor func(int, model.DBReader, model.DAGTraversalManager, model.BlockHeaderStore, + model.GHOSTDAGDataStore) model.PastMedianTimeManager diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go index 1836a3bfb..c609159e5 100644 --- a/domain/consensus/factory.go +++ b/domain/consensus/factory.go @@ -63,22 +63,25 @@ type Factory interface { SetTestGHOSTDAGManager(ghostdagConstructor GHOSTDAGManagerConstructor) SetTestLevelDBCacheSize(cacheSizeMiB int) SetTestPreAllocateCache(preallocateCaches bool) + SetTestPastMedianTimeManager(medianTimeConstructor PastMedianTimeManagerConstructor) SetTestDifficultyManager(difficultyConstructor DifficultyManagerConstructor) } type factory struct { - dataDir string - ghostdagConstructor GHOSTDAGManagerConstructor - difficultyConstructor DifficultyManagerConstructor - cacheSizeMiB *int - preallocateCaches *bool + dataDir string + ghostdagConstructor GHOSTDAGManagerConstructor + pastMedianTimeConsructor PastMedianTimeManagerConstructor + difficultyConstructor DifficultyManagerConstructor + cacheSizeMiB *int + preallocateCaches *bool } // NewFactory creates a new Consensus factory func NewFactory() Factory { return &factory{ - ghostdagConstructor: ghostdagmanager.New, - difficultyConstructor: difficultymanager.New, + ghostdagConstructor: ghostdagmanager.New, + pastMedianTimeConsructor: pastmediantimemanager.New, + difficultyConstructor: difficultymanager.New, } } @@ -147,7 +150,7 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredat reachabilityDataStore, ghostdagManager, consensusStateStore) - pastMedianTimeManager := pastmediantimemanager.New( + pastMedianTimeManager := f.pastMedianTimeConsructor( dagParams.TimestampDeviationTolerance, dbManager, dagTraversalManager, @@ -469,6 +472,10 @@ func (f *factory) SetTestGHOSTDAGManager(ghostdagConstructor GHOSTDAGManagerCons f.ghostdagConstructor = ghostdagConstructor } +func (f *factory) SetTestPastMedianTimeManager(medianTimeConstructor PastMedianTimeManagerConstructor) { + f.pastMedianTimeConsructor = medianTimeConstructor +} + // SetTestDifficultyManager is a setter for the difficultyManager field on the factory. func (f *factory) SetTestDifficultyManager(difficultyConstructor DifficultyManagerConstructor) { f.difficultyConstructor = difficultyConstructor diff --git a/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go b/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go index 847ab3a0d..b7e375626 100644 --- a/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go +++ b/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go @@ -1,6 +1,12 @@ package blockvalidator_test import ( + "bytes" + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/testapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/utils/merkle" + "github.com/kaspanet/kaspad/domain/consensus/utils/utxo" "math" "testing" @@ -84,7 +90,7 @@ func TestChainedTransactions(t *testing.T) { func TestCheckBlockSanity(t *testing.T) { testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { factory := consensus.NewFactory() - consensus, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockSanity") + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockSanity") if err != nil { t.Fatalf("Error setting up consensus: %+v", err) } @@ -94,17 +100,17 @@ func TestCheckBlockSanity(t *testing.T) { t.Fatalf("Too few transactions in block, expect at least 3, got %v", len(exampleValidBlock.Transactions)) } - consensus.BlockStore().Stage(blockHash, &exampleValidBlock) + tc.BlockStore().Stage(blockHash, &exampleValidBlock) - err = consensus.BlockValidator().ValidateBodyInIsolation(blockHash) + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) if err != nil { t.Fatalf("Failed validating block in isolation: %v", err) } // Test with block with wrong transactions sorting order blockHash = consensushashing.BlockHash(&blockWithWrongTxOrder) - consensus.BlockStore().Stage(blockHash, &blockWithWrongTxOrder) - err = consensus.BlockValidator().ValidateBodyInIsolation(blockHash) + tc.BlockStore().Stage(blockHash, &blockWithWrongTxOrder) + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) if !errors.Is(err, ruleerrors.ErrTransactionsNotSorted) { t.Errorf("CheckBlockSanity: Expected ErrTransactionsNotSorted error, instead got %v", err) } @@ -112,8 +118,8 @@ func TestCheckBlockSanity(t *testing.T) { // Test a block with invalid parents order // We no longer require blocks to have ordered parents blockHash = consensushashing.BlockHash(&unOrderedParentsBlock) - consensus.BlockStore().Stage(blockHash, &unOrderedParentsBlock) - err = consensus.BlockValidator().ValidateBodyInIsolation(blockHash) + tc.BlockStore().Stage(blockHash, &unOrderedParentsBlock) + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) if err != nil { t.Errorf("CheckBlockSanity: Expected block to be be body in isolation valid, got error instead: %v", err) } @@ -1034,3 +1040,309 @@ func TestCheckBlockHashMerkleRoot(t *testing.T) { } }) } + +func TestBlockSize(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestBlockSize") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithInvalidBlockSize(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrBlockSizeTooHigh) { + t.Fatalf("ValidateBodyInIsolationTest: TestBlockSize:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrBlockSizeTooHigh, err) + } + }) +} + +func initBlockWithInvalidBlockSize(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + bigSignatureScript := bytes.Repeat([]byte("01"), 25000) + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bigSignatureScript, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + PayloadHash: *externalapi.NewDomainHashFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), + Payload: []byte{0x01}, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, []*externalapi.DomainTransaction{tx}) +} + +func TestCheckBlockDuplicateTransactions(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockDuplicateTransactions") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithDuplicateTransaction(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrDuplicateTx) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckBlockDuplicateTransactions:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrDuplicateTx, err) + } + }) +} + +func initBlockWithDuplicateTransaction(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, []*externalapi.DomainTransaction{tx, tx}) +} + +func TestCheckBlockContainsOnlyOneCoinbase(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockContainsOnlyOneCoinbase") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithMoreThanOneCoinbase(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrMultipleCoinbases) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckBlockContainsOnlyOneCoinbase:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrMultipleCoinbases, err) + } + }) +} + +func initBlockWithMoreThanOneCoinbase(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDCoinbase, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, []*externalapi.DomainTransaction{tx}) +} + +func TestCheckBlockDoubleSpends(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockDoubleSpends") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithDoubleSpends(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrDoubleSpendInSameBlock) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckBlockDoubleSpends:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrDoubleSpendInSameBlock, err) + } + }) +} + +func initBlockWithDoubleSpends(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + txInputSameOutpoint := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("02"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(4)), + } + txSameOutpoint := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInputSameOutpoint}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, + &emptyCoinbase, []*externalapi.DomainTransaction{tx, txSameOutpoint}) +} + +func TestCheckFirstBlockTransactionIsCoinbase(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckFirstBlockTransactionIsCoinbase") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block := initBlockWithFirstTransactionDifferentThanCoinbase(params) + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrFirstTxNotCoinbase) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckFirstBlockTransactionIsCoinbase:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrFirstTxNotCoinbase, err) + } + }) +} + +func initBlockWithFirstTransactionDifferentThanCoinbase(params *dagconfig.Params) *externalapi.DomainBlock { + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + + return &externalapi.DomainBlock{ + Header: blockheader.NewImmutableBlockHeader( + constants.MaxBlockVersion, + []*externalapi.DomainHash{params.GenesisHash}, + merkle.CalculateHashMerkleRoot([]*externalapi.DomainTransaction{tx}), + &externalapi.DomainHash{}, + externalapi.NewDomainHashFromByteArray(&[externalapi.DomainHashSize]byte{ + 0x80, 0xf7, 0x00, 0xe3, 0x16, 0x3d, 0x04, 0x95, + 0x5b, 0x7e, 0xaf, 0x84, 0x7e, 0x1b, 0x6b, 0x06, + 0x4e, 0x06, 0xba, 0x64, 0xd7, 0x61, 0xda, 0x25, + 0x1a, 0x0e, 0x21, 0xd4, 0x64, 0x49, 0x02, 0xa2, + }), + 0x5cd18053000, + 0x207fffff, + 0x1), + Transactions: []*externalapi.DomainTransaction{tx}, + } +} diff --git a/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go new file mode 100644 index 000000000..4b8bf8412 --- /dev/null +++ b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go @@ -0,0 +1,251 @@ +package transactionvalidator_test + +import ( + "github.com/kaspanet/go-secp256k1" + "github.com/kaspanet/kaspad/domain/consensus" + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" + "github.com/kaspanet/kaspad/domain/consensus/utils/txscript" + "github.com/kaspanet/kaspad/domain/consensus/utils/utxo" + "github.com/kaspanet/kaspad/util" + + "math/big" + + "testing" + + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks" + "github.com/kaspanet/kaspad/domain/dagconfig" + "github.com/pkg/errors" +) + +type mocPastMedianTimeManager struct { + pastMedianTimeForTest int64 +} + +// PastMedianTime returns the past median time for the test. +func (mdf *mocPastMedianTimeManager) PastMedianTime(*externalapi.DomainHash) (int64, error) { + return mdf.pastMedianTimeForTest, nil +} + +func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + pastMedianManager := &mocPastMedianTimeManager{} + factory.SetTestPastMedianTimeManager(func(int, model.DBReader, model.DAGTraversalManager, model.BlockHeaderStore, + model.GHOSTDAGDataStore) model.PastMedianTimeManager { + return pastMedianManager + }) + tc, tearDown, err := factory.NewTestConsensus(params, false, + "TestValidateTransactionInContextAndPopulateMassAndFee") + if err != nil { + t.Fatalf("Failed create a NewTestConsensus: %s", err) + } + defer tearDown(false) + + pastMedianManager.pastMedianTimeForTest = 1 + privateKey, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatalf("Failed to generate a private key: %v", err) + } + publicKey, err := privateKey.SchnorrPublicKey() + 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.NewAddressPubKeyHashFromPublicKey(publicKeySerialized[:], params.Prefix) + if err != nil { + t.Fatalf("Failed to generate p2pkh address: %v", err) + } + scriptPublicKey, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("PayToAddrScript: unexpected error: %v", err) + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, // 1 KAS + scriptPublicKey, + true, + uint64(5)), + } + txInputWithMaxSequence := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.SequenceLockTimeIsSeconds, + UTXOEntry: utxo.NewUTXOEntry( + 100000000, // 1 KAS + scriptPublicKey, + true, + uint64(5)), + } + txInputWithLargeAmount := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + constants.MaxSompi, + scriptPublicKey, + true, + uint64(5)), + } + + txOut := externalapi.DomainTransactionOutput{ + Value: 100000000, // 1 KAS + ScriptPublicKey: scriptPublicKey, + } + txOutBigValue := externalapi.DomainTransactionOutput{ + Value: 200_000_000, // 2 KAS + ScriptPublicKey: scriptPublicKey, + } + + validTx := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInputWithMaxSequence}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithImmatureCoinbase := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithLargeAmount := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput, &txInputWithLargeAmount}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithBigValue := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{&txOutBigValue}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithInvalidSignature := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + + for i, input := range validTx.Inputs { + signatureScript, err := txscript.SignatureScript(&validTx, i, scriptPublicKey, txscript.SigHashAll, privateKey) + if err != nil { + t.Fatalf("Failed to create a sigScript: %v", err) + } + input.SignatureScript = signatureScript + } + + povBlockHash := externalapi.NewDomainHashFromByteArray(&[32]byte{0x01}) + genesisHash := params.GenesisHash + tc.GHOSTDAGDataStore().Stage(model.VirtualBlockHash, model.NewBlockGHOSTDAGData( + params.BlockCoinbaseMaturity+txInput.UTXOEntry.BlockBlueScore(), + new(big.Int), + genesisHash, + make([]*externalapi.DomainHash, 1000), + make([]*externalapi.DomainHash, 1), + nil)) + tc.GHOSTDAGDataStore().Stage(povBlockHash, model.NewBlockGHOSTDAGData( + 10, + new(big.Int), + genesisHash, + make([]*externalapi.DomainHash, 1000), + make([]*externalapi.DomainHash, 1), + nil)) + + tests := []struct { + name string + tx *externalapi.DomainTransaction + povBlockHash *externalapi.DomainHash + selectedParentMedianTime int64 + isValid bool + expectedError error + }{ + { + name: "Valid transaction", + tx: &validTx, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: true, + expectedError: nil, + }, + { // The calculated block coinbase maturity is smaller than the minimum expected blockCoinbaseMaturity. + // The povBlockHash blue score is 10 and the UTXO blue score is 5, hence the The subtraction between + // them will yield a smaller result than the required CoinbaseMaturity (currently set to 100). + name: "checkTransactionCoinbaseMaturity", + tx: &txWithImmatureCoinbase, + povBlockHash: povBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrImmatureSpend, + }, + { // The total inputs amount is bigger than the allowed maximum (constants.MaxSompi) + name: "checkTransactionInputAmounts", + tx: &txWithLargeAmount, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrBadTxOutValue, + }, + { // The total SompiIn (sum of inputs amount) is smaller than the total SompiOut (sum of outputs value) and hence invalid. + name: "checkTransactionOutputAmounts", + tx: &txWithBigValue, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrSpendTooHigh, + }, + { // the selectedParentMedianTime is negative and hence invalid. + name: "checkTransactionSequenceLock", + tx: &validTx, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: -1, + isValid: false, + expectedError: ruleerrors.ErrUnfinalizedTx, + }, + { // The SignatureScript (in the txInput) is empty and hence invalid. + name: "checkTransactionScripts", + tx: &txWithInvalidSignature, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrScriptValidation, + }, + } + + for _, test := range tests { + err := tc.TransactionValidator().ValidateTransactionInContextAndPopulateMassAndFee(test.tx, + test.povBlockHash, test.selectedParentMedianTime) + + if test.isValid { + if err != nil { + t.Fatalf("Unexpected error on TestValidateTransactionInContextAndPopulateMassAndFee"+ + " on test %v: %v", test.name, err) + } + } else { + if err == nil || !errors.Is(err, test.expectedError) { + t.Fatalf("TestValidateTransactionInContextAndPopulateMassAndFee: test %v:"+ + " Unexpected error: Expected to: %v, but got : %v", test.name, test.expectedError, err) + } + } + } + }) +}