Fix overflow when checking coinbase maturity and don't ban peers that send transactions with immature spend (#1722)

* Fix overflow when checking coinbase maturity and don't ban peers that send transactions with immature spend

* Fix tests

Co-authored-by: Svarog <feanorr@gmail.com>
(cherry picked from commit a18f2f8802d52261c691ba1051d7b3134ddf8a78)
This commit is contained in:
Ori Newman 2021-05-14 13:02:50 +03:00 committed by stasatdaglabs
parent 9c743db4d6
commit dd3e04e671
5 changed files with 64 additions and 17 deletions

View File

@ -68,8 +68,7 @@ func (v *transactionValidator) checkTransactionCoinbaseMaturity(stagingArea *mod
missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint) missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint)
} else if utxoEntry.IsCoinbase() { } else if utxoEntry.IsCoinbase() {
originDAAScore := utxoEntry.BlockDAAScore() originDAAScore := utxoEntry.BlockDAAScore()
daaScoreSincePrev := povDAAScore - originDAAScore if originDAAScore+v.blockCoinbaseMaturity > povDAAScore {
if daaScoreSincePrev < v.blockCoinbaseMaturity {
return errors.Wrapf(ruleerrors.ErrImmatureSpend, "tried to spend coinbase "+ return errors.Wrapf(ruleerrors.ErrImmatureSpend, "tried to spend coinbase "+
"transaction output %s from DAA score %d "+ "transaction output %s from DAA score %d "+
"to DAA score %d before required maturity "+ "to DAA score %d before required maturity "+

View File

@ -78,7 +78,17 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
true, true,
uint64(5)), 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, PreviousOutpoint: prevOutPoint,
SignatureScript: []byte{}, SignatureScript: []byte{},
Sequence: constants.SequenceLockTimeIsSeconds, Sequence: constants.SequenceLockTimeIsSeconds,
@ -110,7 +120,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
validTx := externalapi.DomainTransaction{ validTx := externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion, Version: constants.MaxTransactionVersion,
Inputs: []*externalapi.DomainTransactionInput{&txInputWithMaxSequence}, Inputs: []*externalapi.DomainTransactionInput{&txInputWithSequenceLockTimeIsSeconds},
Outputs: []*externalapi.DomainTransactionOutput{&txOut}, Outputs: []*externalapi.DomainTransactionOutput{&txOut},
SubnetworkID: subnetworks.SubnetworkIDRegistry, SubnetworkID: subnetworks.SubnetworkIDRegistry,
Gas: 0, Gas: 0,
@ -127,7 +137,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
txWithImmatureCoinbase := externalapi.DomainTransaction{ txWithImmatureCoinbase := externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion, Version: constants.MaxTransactionVersion,
Inputs: []*externalapi.DomainTransactionInput{&txInput}, Inputs: []*externalapi.DomainTransactionInput{&immatureInput},
Outputs: []*externalapi.DomainTransactionOutput{&txOut}, Outputs: []*externalapi.DomainTransactionOutput{&txOut},
SubnetworkID: subnetworks.SubnetworkIDRegistry, SubnetworkID: subnetworks.SubnetworkIDRegistry,
Gas: 0, Gas: 0,
@ -158,7 +168,15 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
povBlockHash := externalapi.NewDomainHashFromByteArray(&[32]byte{0x01}) povBlockHash := externalapi.NewDomainHashFromByteArray(&[32]byte{0x01})
tc.DAABlocksStore().StageDAAScore(stagingArea, povBlockHash, consensusConfig.BlockCoinbaseMaturity+txInput.UTXOEntry.BlockDAAScore()) 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 { tests := []struct {
name string name string
@ -171,7 +189,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
{ {
name: "Valid transaction", name: "Valid transaction",
tx: &validTx, tx: &validTx,
povBlockHash: model.VirtualBlockHash, povBlockHash: povBlockHash,
selectedParentMedianTime: 1, selectedParentMedianTime: 1,
isValid: true, isValid: true,
expectedError: nil, expectedError: nil,
@ -189,7 +207,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
{ // The total inputs amount is bigger than the allowed maximum (constants.MaxSompi) { // The total inputs amount is bigger than the allowed maximum (constants.MaxSompi)
name: "checkTransactionInputAmounts", name: "checkTransactionInputAmounts",
tx: &txWithLargeAmount, tx: &txWithLargeAmount,
povBlockHash: model.VirtualBlockHash, povBlockHash: povBlockHash,
selectedParentMedianTime: 1, selectedParentMedianTime: 1,
isValid: false, isValid: false,
expectedError: ruleerrors.ErrBadTxOutValue, 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. { // The total SompiIn (sum of inputs amount) is smaller than the total SompiOut (sum of outputs value) and hence invalid.
name: "checkTransactionOutputAmounts", name: "checkTransactionOutputAmounts",
tx: &txWithBigValue, tx: &txWithBigValue,
povBlockHash: model.VirtualBlockHash, povBlockHash: povBlockHash,
selectedParentMedianTime: 1, selectedParentMedianTime: 1,
isValid: false, isValid: false,
expectedError: ruleerrors.ErrSpendTooHigh, expectedError: ruleerrors.ErrSpendTooHigh,
@ -205,15 +223,15 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
{ // the selectedParentMedianTime is negative and hence invalid. { // the selectedParentMedianTime is negative and hence invalid.
name: "checkTransactionSequenceLock", name: "checkTransactionSequenceLock",
tx: &validTx, tx: &validTx,
povBlockHash: model.VirtualBlockHash, povBlockHash: povBlockHash,
selectedParentMedianTime: -1, selectedParentMedianTime: -1,
isValid: false, isValid: false,
expectedError: ruleerrors.ErrUnfinalizedTx, 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", name: "checkTransactionScripts",
tx: &txWithInvalidSignature, tx: &txWithInvalidSignature,
povBlockHash: model.VirtualBlockHash, povBlockHash: povBlockHash,
selectedParentMedianTime: 1, selectedParentMedianTime: 1,
isValid: false, isValid: false,
expectedError: ruleerrors.ErrScriptValidation, expectedError: ruleerrors.ErrScriptValidation,
@ -226,7 +244,7 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
if test.isValid { if test.isValid {
if err != nil { if err != nil {
t.Fatalf("Unexpected error on TestValidateTransactionInContextAndPopulateMassAndFee"+ t.Fatalf("Unexpected error on TestValidateTransactionInContextAndPopulateMassAndFee"+
" on test '%v': %v", test.name, err) " on test '%v': %+v", test.name, err)
} }
} else { } else {
if err == nil || !errors.Is(err, test.expectedError) { if err == nil || !errors.Is(err, test.expectedError) {

View File

@ -49,6 +49,7 @@ const (
RejectInsufficientFee RejectCode = 0x42 RejectInsufficientFee RejectCode = 0x42
RejectFinality RejectCode = 0x43 RejectFinality RejectCode = 0x43
RejectDifficulty RejectCode = 0x44 RejectDifficulty RejectCode = 0x44
RejectImmatureSpend RejectCode = 0x45
) )
// Map of reject codes back strings for pretty printing. // Map of reject codes back strings for pretty printing.
@ -63,6 +64,7 @@ var rejectCodeStrings = map[RejectCode]string{
RejectFinality: "REJECT_FINALITY", RejectFinality: "REJECT_FINALITY",
RejectDifficulty: "REJECT_DIFFICULTY", RejectDifficulty: "REJECT_DIFFICULTY",
RejectNotRequested: "REJECT_NOTREQUESTED", RejectNotRequested: "REJECT_NOTREQUESTED",
RejectImmatureSpend: "REJECT_IMMATURESPEND",
} }
// String returns the RejectCode in human-readable form. // String returns the RejectCode in human-readable form.

View File

@ -735,6 +735,9 @@ func (mp *mempool) maybeAcceptTransaction(tx *consensusexternalapi.DomainTransac
if errors.As(err, &missingOutpoints) { if errors.As(err, &missingOutpoints) {
return missingOutpoints.MissingOutpoints, nil, nil 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{}) { if errors.As(err, &ruleerrors.RuleError{}) {
return nil, nil, newRuleError(err) return nil, nil, newRuleError(err)
} }

View File

@ -1,6 +1,7 @@
package miningmanager_test package miningmanager_test
import ( import (
"github.com/kaspanet/kaspad/domain/miningmanager/mempool"
"strings" "strings"
"testing" "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 // TestInsertDoubleTransactionsToMempool verifies that an attempt to insert a transaction
// more than once into the mempool will result in raising an appropriate error. // more than once into the mempool will result in raising an appropriate error.
func TestInsertDoubleTransactionsToMempool(t *testing.T) { func TestInsertDoubleTransactionsToMempool(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
consensusConfig.BlockCoinbaseMaturity = 0
factory := consensus.NewFactory() factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestInsertDoubleTransactionsToMempool") tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestInsertDoubleTransactionsToMempool")
if err != nil { 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. // TestHandleNewBlockTransactions verifies that all the transactions in the block were successfully removed from the mempool.
func TestHandleNewBlockTransactions(t *testing.T) { func TestHandleNewBlockTransactions(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
consensusConfig.BlockCoinbaseMaturity = 0
factory := consensus.NewFactory() factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestHandleNewBlockTransactions") tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestHandleNewBlockTransactions")
if err != nil { if err != nil {
@ -165,7 +190,7 @@ func domainBlocksToBlockIds(blocks []*externalapi.DomainTransaction) []*external
// will be removed from the mempool. // will be removed from the mempool.
func TestDoubleSpends(t *testing.T) { func TestDoubleSpends(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
consensusConfig.BlockCoinbaseMaturity = 0
factory := consensus.NewFactory() factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestDoubleSpends") tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestDoubleSpends")
if err != nil { if err != nil {
@ -309,7 +334,7 @@ func createTransactionWithUTXOEntry(t *testing.T, i int) *externalapi.DomainTran
100000000, // 1 KAS 100000000, // 1 KAS
scriptPublicKey, scriptPublicKey,
true, true,
uint64(5)), uint64(0)),
} }
txOut := externalapi.DomainTransactionOutput{ txOut := externalapi.DomainTransactionOutput{
Value: 10000, Value: 10000,