diff --git a/domain/consensus/processes/transactionvalidator/transaction_in_context.go b/domain/consensus/processes/transactionvalidator/transaction_in_context.go index 4f7b8bb01..a9a16ec45 100644 --- a/domain/consensus/processes/transactionvalidator/transaction_in_context.go +++ b/domain/consensus/processes/transactionvalidator/transaction_in_context.go @@ -68,8 +68,7 @@ func (v *transactionValidator) checkTransactionCoinbaseMaturity(stagingArea *mod missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint) } else if utxoEntry.IsCoinbase() { originDAAScore := utxoEntry.BlockDAAScore() - daaScoreSincePrev := povDAAScore - originDAAScore - if daaScoreSincePrev < v.blockCoinbaseMaturity { + if originDAAScore+v.blockCoinbaseMaturity > povDAAScore { return errors.Wrapf(ruleerrors.ErrImmatureSpend, "tried to spend coinbase "+ "transaction output %s from DAA score %d "+ "to DAA score %d before required maturity "+ diff --git a/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go index 6327d3ada..81c95e49c 100644 --- a/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go +++ b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go @@ -78,7 +78,17 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { true, uint64(5)), } - txInputWithMaxSequence := externalapi.DomainTransactionInput{ + immatureInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, // 1 KAS + scriptPublicKey, + true, + uint64(6)), + } + txInputWithSequenceLockTimeIsSeconds := externalapi.DomainTransactionInput{ PreviousOutpoint: prevOutPoint, SignatureScript: []byte{}, Sequence: constants.SequenceLockTimeIsSeconds, @@ -110,7 +120,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { validTx := externalapi.DomainTransaction{ Version: constants.MaxTransactionVersion, - Inputs: []*externalapi.DomainTransactionInput{&txInputWithMaxSequence}, + Inputs: []*externalapi.DomainTransactionInput{&txInputWithSequenceLockTimeIsSeconds}, Outputs: []*externalapi.DomainTransactionOutput{&txOut}, SubnetworkID: subnetworks.SubnetworkIDRegistry, Gas: 0, @@ -127,7 +137,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { txWithImmatureCoinbase := externalapi.DomainTransaction{ Version: constants.MaxTransactionVersion, - Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Inputs: []*externalapi.DomainTransactionInput{&immatureInput}, Outputs: []*externalapi.DomainTransactionOutput{&txOut}, SubnetworkID: subnetworks.SubnetworkIDRegistry, Gas: 0, @@ -158,7 +168,15 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { povBlockHash := externalapi.NewDomainHashFromByteArray(&[32]byte{0x01}) tc.DAABlocksStore().StageDAAScore(stagingArea, povBlockHash, consensusConfig.BlockCoinbaseMaturity+txInput.UTXOEntry.BlockDAAScore()) - tc.DAABlocksStore().StageDAAScore(stagingArea, povBlockHash, 10) + + // Just use some stub ghostdag data + tc.GHOSTDAGDataStore().Stage(stagingArea, povBlockHash, model.NewBlockGHOSTDAGData( + 0, + nil, + consensusConfig.GenesisHash, + nil, + nil, + nil)) tests := []struct { name string @@ -171,7 +189,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { { name: "Valid transaction", tx: &validTx, - povBlockHash: model.VirtualBlockHash, + povBlockHash: povBlockHash, selectedParentMedianTime: 1, isValid: true, expectedError: nil, @@ -189,7 +207,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { { // The total inputs amount is bigger than the allowed maximum (constants.MaxSompi) name: "checkTransactionInputAmounts", tx: &txWithLargeAmount, - povBlockHash: model.VirtualBlockHash, + povBlockHash: povBlockHash, selectedParentMedianTime: 1, isValid: false, expectedError: ruleerrors.ErrBadTxOutValue, @@ -197,7 +215,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { { // 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, + povBlockHash: povBlockHash, selectedParentMedianTime: 1, isValid: false, expectedError: ruleerrors.ErrSpendTooHigh, @@ -205,15 +223,15 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { { // the selectedParentMedianTime is negative and hence invalid. name: "checkTransactionSequenceLock", tx: &validTx, - povBlockHash: model.VirtualBlockHash, + povBlockHash: povBlockHash, selectedParentMedianTime: -1, isValid: false, expectedError: ruleerrors.ErrUnfinalizedTx, }, - { // The SignatureScript (in the txInput) is empty and hence invalid. + { // The SignatureScript (in the immatureInput) is empty and hence invalid. name: "checkTransactionScripts", tx: &txWithInvalidSignature, - povBlockHash: model.VirtualBlockHash, + povBlockHash: povBlockHash, selectedParentMedianTime: 1, isValid: false, expectedError: ruleerrors.ErrScriptValidation, @@ -226,7 +244,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { if test.isValid { if err != nil { t.Fatalf("Unexpected error on TestValidateTransactionInContextAndPopulateMassAndFee"+ - " on test '%v': %v", test.name, err) + " on test '%v': %+v", test.name, err) } } else { if err == nil || !errors.Is(err, test.expectedError) { diff --git a/domain/miningmanager/mempool/error.go b/domain/miningmanager/mempool/error.go index 001bb4daa..bc51bb3a0 100644 --- a/domain/miningmanager/mempool/error.go +++ b/domain/miningmanager/mempool/error.go @@ -49,6 +49,7 @@ const ( RejectInsufficientFee RejectCode = 0x42 RejectFinality RejectCode = 0x43 RejectDifficulty RejectCode = 0x44 + RejectImmatureSpend RejectCode = 0x45 ) // Map of reject codes back strings for pretty printing. @@ -63,6 +64,7 @@ var rejectCodeStrings = map[RejectCode]string{ RejectFinality: "REJECT_FINALITY", RejectDifficulty: "REJECT_DIFFICULTY", RejectNotRequested: "REJECT_NOTREQUESTED", + RejectImmatureSpend: "REJECT_IMMATURESPEND", } // String returns the RejectCode in human-readable form. diff --git a/domain/miningmanager/mempool/mempool.go b/domain/miningmanager/mempool/mempool.go index 9ff7c9d67..79576dc8a 100644 --- a/domain/miningmanager/mempool/mempool.go +++ b/domain/miningmanager/mempool/mempool.go @@ -720,6 +720,9 @@ func (mp *mempool) maybeAcceptTransaction(tx *consensusexternalapi.DomainTransac if errors.As(err, &missingOutpoints) { return missingOutpoints.MissingOutpoints, nil, nil } + if errors.Is(err, ruleerrors.ErrImmatureSpend) { + return nil, nil, txRuleError(RejectImmatureSpend, "one of the transaction inputs spends an immature UTXO") + } if errors.As(err, &ruleerrors.RuleError{}) { return nil, nil, newRuleError(err) } diff --git a/domain/miningmanager/miningmanager_test.go b/domain/miningmanager/miningmanager_test.go index 70359aa06..4cde5d3d1 100644 --- a/domain/miningmanager/miningmanager_test.go +++ b/domain/miningmanager/miningmanager_test.go @@ -1,6 +1,7 @@ package miningmanager_test import ( + "github.com/kaspanet/kaspad/domain/miningmanager/mempool" "strings" "testing" @@ -70,11 +71,35 @@ func TestValidateAndInsertTransaction(t *testing.T) { }) } +func TestImmatureSpend(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestValidateAndInsertTransaction") + if err != nil { + t.Fatalf("Error setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + miningManager := miningFactory.NewMiningManager(tc, &consensusConfig.Params) + tx := createTransactionWithUTXOEntry(t, 0) + err = miningManager.ValidateAndInsertTransaction(tx, false) + txRuleError := &mempool.TxRuleError{} + if !errors.As(err, txRuleError) || txRuleError.RejectCode != mempool.RejectImmatureSpend { + t.Fatalf("Unexpected error %+v", err) + } + transactionsFromMempool := miningManager.AllTransactions() + if contains(tx, transactionsFromMempool) { + t.Fatalf("Mempool contains a transaction with immature coinbase") + } + }) +} + // 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, consensusConfig *consensus.Config) { - + consensusConfig.BlockCoinbaseMaturity = 0 factory := consensus.NewFactory() tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestInsertDoubleTransactionsToMempool") if err != nil { @@ -99,7 +124,7 @@ func TestInsertDoubleTransactionsToMempool(t *testing.T) { // 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, consensusConfig *consensus.Config) { - + consensusConfig.BlockCoinbaseMaturity = 0 factory := consensus.NewFactory() tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestHandleNewBlockTransactions") if err != nil { @@ -165,7 +190,7 @@ func domainBlocksToBlockIds(blocks []*externalapi.DomainTransaction) []*external // will be removed from the mempool. func TestDoubleSpends(t *testing.T) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { - + consensusConfig.BlockCoinbaseMaturity = 0 factory := consensus.NewFactory() tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestDoubleSpends") if err != nil { @@ -309,7 +334,7 @@ func createTransactionWithUTXOEntry(t *testing.T, i int) *externalapi.DomainTran 100000000, // 1 KAS scriptPublicKey, true, - uint64(5)), + uint64(0)), } txOut := externalapi.DomainTransactionOutput{ Value: 10000,