diff --git a/domain/consensus/processes/blockvalidator/block_body_in_context_test.go b/domain/consensus/processes/blockvalidator/block_body_in_context_test.go index 99c4e1dc9..4b3e54070 100644 --- a/domain/consensus/processes/blockvalidator/block_body_in_context_test.go +++ b/domain/consensus/processes/blockvalidator/block_body_in_context_test.go @@ -199,7 +199,7 @@ func TestIsFinalizedTransaction(t *testing.T) { } checkForLockTimeAndSequence := func(lockTime, sequence uint64, shouldPass bool) { - tx, err := testutils.CreateTransaction(parentToSpend.Transactions[0]) + tx, err := testutils.CreateTransaction(parentToSpend.Transactions[0], 1) if err != nil { t.Fatalf("Error creating tx: %+v", err) } 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 1741c7f7b..35a88ffbf 100644 --- a/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go +++ b/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go @@ -49,12 +49,12 @@ func TestChainedTransactions(t *testing.T) { t.Fatalf("Error getting block1: %+v", err) } - tx1, err := testutils.CreateTransaction(block1.Transactions[0]) + tx1, err := testutils.CreateTransaction(block1.Transactions[0], 1) if err != nil { t.Fatalf("Error creating tx1: %+v", err) } - chainedTx, err := testutils.CreateTransaction(tx1) + chainedTx, err := testutils.CreateTransaction(tx1, 1) if err != nil { t.Fatalf("Error creating chainedTx: %+v", err) } @@ -76,7 +76,7 @@ func TestChainedTransactions(t *testing.T) { t.Fatalf("Error getting block2: %+v", err) } - tx2, err := testutils.CreateTransaction(block2.Transactions[0]) + tx2, err := testutils.CreateTransaction(block2.Transactions[0], 1) if err != nil { t.Fatalf("Error creating tx2: %+v", err) } diff --git a/domain/consensus/processes/consensusstatemanager/calculate_past_utxo_test.go b/domain/consensus/processes/consensusstatemanager/calculate_past_utxo_test.go index 6e86e5e2e..bb50a9bc5 100644 --- a/domain/consensus/processes/consensusstatemanager/calculate_past_utxo_test.go +++ b/domain/consensus/processes/consensusstatemanager/calculate_past_utxo_test.go @@ -57,7 +57,7 @@ func TestUTXOCommitment(t *testing.T) { checkBlockUTXOCommitment(t, consensus, blockCHash, "C") // Block D: blockDTransaction, err := testutils.CreateTransaction( - blockB.Transactions[transactionhelper.CoinbaseTransactionIndex]) + blockB.Transactions[transactionhelper.CoinbaseTransactionIndex], 1) if err != nil { t.Fatalf("Error creating transaction: %+v", err) } diff --git a/domain/consensus/processes/consensusstatemanager/resolve_block_status_test.go b/domain/consensus/processes/consensusstatemanager/resolve_block_status_test.go index 755985d5a..c3b95834a 100644 --- a/domain/consensus/processes/consensusstatemanager/resolve_block_status_test.go +++ b/domain/consensus/processes/consensusstatemanager/resolve_block_status_test.go @@ -51,11 +51,11 @@ func TestDoubleSpends(t *testing.T) { fundingTransaction := fundingBlock.Transactions[transactionhelper.CoinbaseTransactionIndex] // Create two transactions that spends the same output, but with different IDs - spendingTransaction1, err := testutils.CreateTransaction(fundingTransaction) + spendingTransaction1, err := testutils.CreateTransaction(fundingTransaction, 1) if err != nil { t.Fatalf("Error creating spendingTransaction1: %+v", err) } - spendingTransaction2, err := testutils.CreateTransaction(fundingTransaction) + spendingTransaction2, err := testutils.CreateTransaction(fundingTransaction, 1) if err != nil { t.Fatalf("Error creating spendingTransaction2: %+v", err) } @@ -213,7 +213,7 @@ func TestTransactionAcceptance(t *testing.T) { fundingTransaction2 := fundingBlock3.Transactions[transactionhelper.CoinbaseTransactionIndex] - spendingTransaction1, err := testutils.CreateTransaction(fundingTransaction1) + spendingTransaction1, err := testutils.CreateTransaction(fundingTransaction1, 1) if err != nil { t.Fatalf("Error creating spendingTransaction1: %+v", err) } @@ -223,7 +223,7 @@ func TestTransactionAcceptance(t *testing.T) { t.Fatalf("Error getting UTXOEntry for spendingTransaction1: %s", err) } - spendingTransaction2, err := testutils.CreateTransaction(fundingTransaction2) + spendingTransaction2, err := testutils.CreateTransaction(fundingTransaction2, 1) if err != nil { t.Fatalf("Error creating spendingTransaction1: %+v", err) } diff --git a/domain/consensus/utils/testutils/create_transaction.go b/domain/consensus/utils/testutils/create_transaction.go index c747d20fc..a5e219643 100644 --- a/domain/consensus/utils/testutils/create_transaction.go +++ b/domain/consensus/utils/testutils/create_transaction.go @@ -10,7 +10,7 @@ import ( // CreateTransaction create a transaction that spends the first output of provided transaction. // Assumes that the output being spent has opTrueScript as it's scriptPublicKey // Creates the value of the spent output minus 1 sompi -func CreateTransaction(txToSpend *externalapi.DomainTransaction) (*externalapi.DomainTransaction, error) { +func CreateTransaction(txToSpend *externalapi.DomainTransaction, fee uint64) (*externalapi.DomainTransaction, error) { scriptPublicKey, redeemScript := OpTrueScript() signatureScript, err := txscript.PayToScriptHashSignatureScript(redeemScript, nil) @@ -27,7 +27,7 @@ func CreateTransaction(txToSpend *externalapi.DomainTransaction) (*externalapi.D } output := &externalapi.DomainTransactionOutput{ ScriptPublicKey: scriptPublicKey, - Value: txToSpend.Outputs[0].Value - 1, + Value: txToSpend.Outputs[0].Value - fee, } return &externalapi.DomainTransaction{ Version: constants.MaxTransactionVersion, diff --git a/domain/miningmanager/miningmanager_test.go b/domain/miningmanager/miningmanager_test.go new file mode 100644 index 000000000..f1ee75dce --- /dev/null +++ b/domain/miningmanager/miningmanager_test.go @@ -0,0 +1,418 @@ +package miningmanager_test + +import ( + "github.com/kaspanet/kaspad/domain/consensus/model/testapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" + "github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper" + "github.com/kaspanet/kaspad/domain/miningmanager" + "github.com/pkg/errors" + "strings" + + "github.com/kaspanet/kaspad/domain/consensus" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/utxo" + + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" + "github.com/kaspanet/kaspad/domain/consensus/utils/txscript" + "github.com/kaspanet/kaspad/domain/dagconfig" + + "testing" +) + +const blockMaxMass uint64 = 10000000 + +// TestValidateAndInsertTransaction verifies that valid transactions were successfully inserted into the mempool. +func TestValidateAndInsertTransaction(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, "TestValidateAndInsertTransaction") + if err != nil { + t.Fatalf("Error setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + miningManager := miningFactory.NewMiningManager(tc, blockMaxMass, false) + transactionsToInsert := make([]*externalapi.DomainTransaction, 10) + for i := range transactionsToInsert { + transactionsToInsert[i] = createTransactionWithUTXOEntry(t, i) + err = miningManager.ValidateAndInsertTransaction(transactionsToInsert[i], true) + if err != nil { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + } + // The UTXOEntry was filled manually for those transactions, so the transactions won't be considered orphans. + // Therefore, all the transactions expected to be contained in the mempool. + transactionsFromMempool := miningManager.AllTransactions() + if len(transactionsToInsert) != len(transactionsFromMempool) { + t.Fatalf("Wrong number of transactions in mempool: expected: %d, got: %d", len(transactionsToInsert), len(transactionsFromMempool)) + } + for _, transactionToInsert := range transactionsToInsert { + if !contains(transactionToInsert, transactionsFromMempool) { + t.Fatalf("Missing transaction %s in the mempool", consensushashing.TransactionID(transactionToInsert)) + } + } + + // The parent's transaction was inserted by consensus(AddBlock), and we want to verify that + // the transaction is not considered an orphan and inserted into the mempool. + transactionNotAnOrphan, err := createChildTxWhenParentTxWasAddedByConsensus(params, tc) + if err != nil { + t.Fatalf("Error in createParentAndChildrenTransaction: %v", err) + } + err = miningManager.ValidateAndInsertTransaction(transactionNotAnOrphan, true) + if err != nil { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + transactionsFromMempool = miningManager.AllTransactions() + if !contains(transactionNotAnOrphan, transactionsFromMempool) { + t.Fatalf("Missing transaction %s in the mempool", consensushashing.TransactionID(transactionNotAnOrphan)) + } + }) +} + +// TestInsertDoubleTransactionsToMempool verifies that an attempt to insert a transaction +// more than once into the mempool will result in raising an appropriate error. +func TestInsertDoubleTransactionsToMempool(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestInsertDoubleTransactionsToMempool") + if err != nil { + t.Fatalf("Error setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + miningManager := miningFactory.NewMiningManager(tc, blockMaxMass, false) + transaction := createTransactionWithUTXOEntry(t, 0) + err = miningManager.ValidateAndInsertTransaction(transaction, true) + if err != nil { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + err = miningManager.ValidateAndInsertTransaction(transaction, true) + if err == nil || !strings.Contains(err.Error(), "already have transaction") { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + }) +} + +// TestHandleNewBlockTransactions verifies that all the transactions in the block were successfully removed from the mempool. +func TestHandleNewBlockTransactions(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestHandleNewBlockTransactions") + if err != nil { + t.Fatalf("Error setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + miningManager := miningFactory.NewMiningManager(tc, blockMaxMass, false) + transactionsToInsert := make([]*externalapi.DomainTransaction, 10) + for i := range transactionsToInsert { + transaction := createTransactionWithUTXOEntry(t, i) + transactionsToInsert[i] = transaction + err = miningManager.ValidateAndInsertTransaction(transaction, true) + if err != nil { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + } + + const partialLength = 3 + blockWithFirstPartOfTheTransactions := append([]*externalapi.DomainTransaction{nil}, transactionsToInsert[0:partialLength]...) + blockWithRestOfTheTransactions := append([]*externalapi.DomainTransaction{nil}, transactionsToInsert[partialLength:]...) + _, err = miningManager.HandleNewBlockTransactions(blockWithFirstPartOfTheTransactions) + if err != nil { + t.Fatalf("HandleNewBlockTransactions: %v", err) + } + mempoolTransactions := miningManager.AllTransactions() + for _, removedTransaction := range blockWithFirstPartOfTheTransactions { + if contains(removedTransaction, mempoolTransactions) { + t.Fatalf("This transaction shouldnt be in mempool: %s", consensushashing.TransactionID(removedTransaction)) + } + } + + // There are no chained/double-spends transactions, and hence it is expected that all the other + // transactions, will still be included in the mempool. + mempoolTransactions = miningManager.AllTransactions() + for _, transaction := range blockWithRestOfTheTransactions[transactionhelper.CoinbaseTransactionIndex+1:] { + if !contains(transaction, mempoolTransactions) { + t.Fatalf("This transaction %s should be in mempool.", consensushashing.TransactionID(transaction)) + } + } + // Handle all the other transactions. + _, err = miningManager.HandleNewBlockTransactions(blockWithRestOfTheTransactions) + if err != nil { + t.Fatalf("HandleNewBlockTransactions: %v", err) + } + if len(miningManager.AllTransactions()) != 0 { + blockIDs := domainBlocksToBlockIds(miningManager.AllTransactions()) + t.Fatalf("The mempool contains unexpected transactions: %s", blockIDs) + } + }) +} + +func domainBlocksToBlockIds(blocks []*externalapi.DomainTransaction) []*externalapi.DomainTransactionID { + blockIDs := make([]*externalapi.DomainTransactionID, len(blocks)) + for i := range blockIDs { + blockIDs[i] = consensushashing.TransactionID(blocks[i]) + } + return blockIDs +} + +// TestDoubleSpends verifies that any transactions which are now double spends as a result of the block's new transactions +// will be removed from the mempool. +func TestDoubleSpends(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestDoubleSpends") + if err != nil { + t.Fatalf("Failed setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + miningManager := miningFactory.NewMiningManager(tc, blockMaxMass, false) + transactionInTheMempool := createTransactionWithUTXOEntry(t, 0) + err = miningManager.ValidateAndInsertTransaction(transactionInTheMempool, true) + if err != nil { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + doubleSpendTransactionInTheBlock := createTransactionWithUTXOEntry(t, 0) + doubleSpendTransactionInTheBlock.Inputs[0].PreviousOutpoint = transactionInTheMempool.Inputs[0].PreviousOutpoint + blockTransactions := []*externalapi.DomainTransaction{nil, doubleSpendTransactionInTheBlock} + _, err = miningManager.HandleNewBlockTransactions(blockTransactions) + if err != nil { + t.Fatalf("HandleNewBlockTransactions: %v", err) + } + if contains(transactionInTheMempool, miningManager.AllTransactions()) { + t.Fatalf("The transaction %s, shouldn't be in the mempool, since at least one "+ + "output was already spent.", consensushashing.TransactionID(transactionInTheMempool)) + } + }) +} + +// TestOrphanTransactions verifies that a transaction could be a part of a new block template, only if it's not an orphan. +func TestOrphanTransactions(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, "TestOrphanTransactions") + if err != nil { + t.Fatalf("Error setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + miningManager := miningFactory.NewMiningManager(tc, blockMaxMass, false) + // Before each parent transaction, We will add two blocks by consensus in order to fund the parent transactions. + parentTransactions, childTransactions, err := createArraysOfParentAndChildrenTransactions(tc) + if err != nil { + t.Fatalf("Error in createArraysOfParentAndChildrenTransactions: %v", err) + } + for _, orphanTransaction := range childTransactions { + err = miningManager.ValidateAndInsertTransaction(orphanTransaction, true) + if err != nil { + t.Fatalf("ValidateAndInsertTransaction: %v", err) + } + } + transactionsMempool := miningManager.AllTransactions() + for _, transaction := range transactionsMempool { + if contains(transaction, childTransactions) { + t.Fatalf("Error: an orphan transaction is exist in the mempool") + } + } + + block, err := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{Script: nil, Version: 0}, + ExtraData: nil}) + if err != nil { + t.Fatalf("Failed get a block template: %v", err) + } + for _, transactionFromBlock := range block.Transactions[1:] { + for _, orphanTransaction := range childTransactions { + if consensushashing.TransactionID(transactionFromBlock) == consensushashing.TransactionID(orphanTransaction) { + t.Fatalf("Tranasaction with unknown parents is exist in a block that was built from GetTemplate option.") + } + } + } + tips, err := tc.Tips() + if err != nil { + t.Fatalf("Tips: %v.", err) + } + blockParentsTransactionsHash, _, err := tc.AddBlock(tips, nil, parentTransactions) + if err != nil { + t.Fatalf("AddBlock: %v", err) + } + + _, _, err = tc.AddBlock([]*externalapi.DomainHash{blockParentsTransactionsHash}, nil, nil) + if err != nil { + t.Fatalf("AddBlock: %v", err) + } + + blockParentsTransactions, err := tc.GetBlock(blockParentsTransactionsHash) + if err != nil { + t.Fatalf("GetBlock: %v", err) + } + _, err = miningManager.HandleNewBlockTransactions(blockParentsTransactions.Transactions) + if err != nil { + t.Fatalf("HandleNewBlockTransactions: %v", err) + } + transactionsMempool = miningManager.AllTransactions() + if len(transactionsMempool) != len(childTransactions) { + t.Fatalf("Expected %d transactions in the mempool but got %d", len(childTransactions), len(transactionsMempool)) + } + + for _, transaction := range transactionsMempool { + if !contains(transaction, childTransactions) { + t.Fatalf("Error: the transaction %s, should be in the mempool since its not "+ + "oprhan anymore.", consensushashing.TransactionID(transaction)) + } + } + block, err = miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{Script: nil, Version: 0}, + ExtraData: nil}) + if err != nil { + t.Fatalf("GetBlockTemplate: %v", err) + } + for _, transactionFromBlock := range block.Transactions[1:] { + isContained := false + for _, childTransaction := range childTransactions { + if *consensushashing.TransactionID(transactionFromBlock) == *consensushashing.TransactionID(childTransaction) { + isContained = true + break + } + } + if !isContained { + t.Fatalf("Error: Unknown Transaction %s in a block.", consensushashing.TransactionID(transactionFromBlock)) + } + } + }) +} + +func createTransactionWithUTXOEntry(t *testing.T, i int) *externalapi.DomainTransaction { + prevOutTxID := externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: prevOutTxID, Index: uint32(i)} + scriptPublicKey, redeemScript := testutils.OpTrueScript() + signatureScript, err := txscript.PayToScriptHashSignatureScript(redeemScript, nil) + if err != nil { + t.Fatalf("PayToScriptHashSignatureScript: %v", err) + } + txInputWithMaxSequence := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: signatureScript, + Sequence: constants.SequenceLockTimeIsSeconds, + UTXOEntry: utxo.NewUTXOEntry( + 100000000, // 1 KAS + scriptPublicKey, + true, + uint64(5)), + } + txOut := externalapi.DomainTransactionOutput{ + Value: 10000, + ScriptPublicKey: scriptPublicKey, + } + tx := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInputWithMaxSequence}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDNative, + Gas: 0, + Fee: 289, + Mass: 1, + LockTime: 0} + + return &tx +} + +func createArraysOfParentAndChildrenTransactions(tc testapi.TestConsensus) ([]*externalapi.DomainTransaction, + []*externalapi.DomainTransaction, error) { + + const numOfTransactions = 5 + transactions := make([]*externalapi.DomainTransaction, numOfTransactions) + parentTransactions := make([]*externalapi.DomainTransaction, len(transactions)) + var err error + for i := range transactions { + parentTransactions[i], transactions[i], err = createParentAndChildrenTransactions(tc) + if err != nil { + return nil, nil, err + } + } + return parentTransactions, transactions, nil +} + +func createParentAndChildrenTransactions(tc testapi.TestConsensus) (*externalapi.DomainTransaction, + *externalapi.DomainTransaction, error) { + + // We will add two blocks by consensus before the parent transactions, in order to fund the parent transactions. + tips, err := tc.Tips() + if err != nil { + return nil, nil, err + } + + _, _, err = tc.AddBlock(tips, nil, nil) + if err != nil { + return nil, nil, errors.Wrapf(err, "AddBlock: %v", err) + } + + tips, err = tc.Tips() + if err != nil { + return nil, nil, err + } + + fundingBlockHashForParent, _, err := tc.AddBlock(tips, nil, nil) + if err != nil { + return nil, nil, errors.Wrap(err, "AddBlock: ") + } + fundingBlockForParent, err := tc.GetBlock(fundingBlockHashForParent) + if err != nil { + return nil, nil, errors.Wrap(err, "GetBlock: ") + } + fundingTransactionForParent := fundingBlockForParent.Transactions[transactionhelper.CoinbaseTransactionIndex] + txParent, err := testutils.CreateTransaction(fundingTransactionForParent, 1000) + if err != nil { + return nil, nil, err + } + txChild, err := testutils.CreateTransaction(txParent, 1000) + if err != nil { + return nil, nil, err + } + return txParent, txChild, nil +} + +func createChildTxWhenParentTxWasAddedByConsensus(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainTransaction, error) { + + firstBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, nil, nil) + if err != nil { + return nil, errors.Wrapf(err, "AddBlock: %v", err) + } + ParentBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{firstBlockHash}, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "AddBlock: ") + } + ParentBlock, err := tc.GetBlock(ParentBlockHash) + if err != nil { + return nil, errors.Wrap(err, "GetBlock: ") + } + parentTransaction := ParentBlock.Transactions[transactionhelper.CoinbaseTransactionIndex] + txChild, err := testutils.CreateTransaction(parentTransaction, 1000) + if err != nil { + return nil, err + } + return txChild, nil +} + +func contains(transaction *externalapi.DomainTransaction, transactions []*externalapi.DomainTransaction) bool { + for _, candidateTransaction := range transactions { + if candidateTransaction.Equal(transaction) { + return true + } + } + return false +}