diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 7c8d1b23c..1525aae98 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -23,7 +23,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: 1.19 + go-version: 1.21 - name: Build on Linux if: runner.os == 'Linux' diff --git a/cmd/kaspawallet/config.go b/cmd/kaspawallet/config.go index 6313167bf..cd0ddb7a2 100644 --- a/cmd/kaspawallet/config.go +++ b/cmd/kaspawallet/config.go @@ -57,7 +57,7 @@ type sendConfig struct { DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"` ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"` FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"` - SendAmount float64 `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"` + SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"` IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount)"` UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"` Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"` @@ -74,7 +74,7 @@ type createUnsignedTransactionConfig struct { DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"` ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"` FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"` - SendAmount float64 `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"` + SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"` IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount)"` UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"` config.NetworkFlags @@ -296,8 +296,8 @@ func parseCommandLine() (subCommand string, config interface{}) { } func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig) error { - if (!conf.IsSendAll && conf.SendAmount == 0) || - (conf.IsSendAll && conf.SendAmount > 0) { + if (!conf.IsSendAll && conf.SendAmount == "") || + (conf.IsSendAll && conf.SendAmount != "") { return errors.New("exactly one of '--send-amount' or '--all' must be specified") } @@ -305,8 +305,8 @@ func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig } func validateSendConfig(conf *sendConfig) error { - if (!conf.IsSendAll && conf.SendAmount == 0) || - (conf.IsSendAll && conf.SendAmount > 0) { + if (!conf.IsSendAll && conf.SendAmount == "") || + (conf.IsSendAll && conf.SendAmount != "") { return errors.New("exactly one of '--send-amount' or '--all' must be specified") } diff --git a/cmd/kaspawallet/create_unsigned_tx.go b/cmd/kaspawallet/create_unsigned_tx.go index 8f201685a..f213bc958 100644 --- a/cmd/kaspawallet/create_unsigned_tx.go +++ b/cmd/kaspawallet/create_unsigned_tx.go @@ -7,7 +7,7 @@ import ( "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" - "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/cmd/kaspawallet/utils" ) func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error { @@ -20,7 +20,12 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error { ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout) defer cancel() - sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa) + sendAmountSompi, err := utils.KasToSompi(conf.SendAmount) + + if err != nil { + return err + } + response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{ From: conf.FromAddresses, Address: conf.ToAddress, diff --git a/cmd/kaspawallet/send.go b/cmd/kaspawallet/send.go index 3e20c2b97..0ac8f78ab 100644 --- a/cmd/kaspawallet/send.go +++ b/cmd/kaspawallet/send.go @@ -10,7 +10,7 @@ import ( "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/keys" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" - "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/cmd/kaspawallet/utils" "github.com/pkg/errors" ) @@ -35,7 +35,11 @@ func send(conf *sendConfig) error { var sendAmountSompi uint64 if !conf.IsSendAll { - sendAmountSompi = uint64(conf.SendAmount * constants.SompiPerKaspa) + sendAmountSompi, err = utils.KasToSompi(conf.SendAmount) + + if err != nil { + return err + } } createUnsignedTransactionsResponse, err := diff --git a/cmd/kaspawallet/utils/format_kas.go b/cmd/kaspawallet/utils/format_kas.go index 5b1de0bea..e1f396dd5 100644 --- a/cmd/kaspawallet/utils/format_kas.go +++ b/cmd/kaspawallet/utils/format_kas.go @@ -2,8 +2,13 @@ package utils import ( "fmt" + "math" + "regexp" + "strconv" + "strings" "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/pkg/errors" ) // FormatKas takes the amount of sompis as uint64, and returns amount of KAS with 8 decimal places @@ -14,3 +19,50 @@ func FormatKas(amount uint64) string { } return res } + +// KasToSompi takes in a string representation of the Kas value to convert to Sompi +func KasToSompi(amount string) (uint64, error) { + err := validateKASAmountFormat(amount) + + if err != nil { + return 0, err + } + + // after validation, amount can only be either an int OR + // a float with an int component and decimal places + parts := strings.Split(amount, ".") + amountStr := "" + + if constants.SompiPerKaspa%10 != 0 { + return 0, errors.Errorf("Unable to convert to sompi when SompiPerKaspa is not a multiple of 10") + } + + decimalPlaces := int(math.Log10(constants.SompiPerKaspa)) + decimalStr := "" + + if len(parts) == 2 { + decimalStr = parts[1] + } + + amountStr = fmt.Sprintf("%s%-*s", parts[0], decimalPlaces, decimalStr) // Padded with spaces at the end to fill for missing decimals: Sample "0.01234 " + amountStr = strings.ReplaceAll(amountStr, " ", "0") // Make the spaces be 0s. Sample "0.012340000" + + convertedAmount, err := strconv.ParseUint(amountStr, 10, 64) + + return convertedAmount, err +} + +func validateKASAmountFormat(amount string) error { + // Check whether it's an integer, or a float with max 8 digits + match, err := regexp.MatchString("^([1-9]\\d{0,11}|0)(\\.\\d{0,8})?$", amount) + + if !match { + return errors.Errorf("Invalid amount") + } + + if err != nil { + return err + } + + return nil +} diff --git a/cmd/kaspawallet/utils/format_kas_test.go b/cmd/kaspawallet/utils/format_kas_test.go new file mode 100644 index 000000000..11646e942 --- /dev/null +++ b/cmd/kaspawallet/utils/format_kas_test.go @@ -0,0 +1,90 @@ +package utils + +import "testing" + +// Takes in a string representation of the Kas value to convert to Sompi +func TestKasToSompi(t *testing.T) { + type testVector struct { + originalAmount string + convertedAmount uint64 + } + + validCases := []testVector{ + {originalAmount: "0", convertedAmount: 0}, + {originalAmount: "1", convertedAmount: 100000000}, + {originalAmount: "33184.1489732", convertedAmount: 3318414897320}, + {originalAmount: "21.35808032", convertedAmount: 2135808032}, + {originalAmount: "184467440737.09551615", convertedAmount: 18446744073709551615}, + } + + for _, currentTestVector := range validCases { + convertedAmount, err := KasToSompi(currentTestVector.originalAmount) + + if err != nil { + t.Error(err) + } else if convertedAmount != currentTestVector.convertedAmount { + t.Errorf("Expected %s, to convert to %d. Got: %d", currentTestVector.originalAmount, currentTestVector.convertedAmount, convertedAmount) + } + } + + invalidCases := []string{ + "184467440737.09551616", // Bigger than max uint64 + "-1", + "a", + "", + } + + for _, currentTestVector := range invalidCases { + _, err := KasToSompi(currentTestVector) + + if err == nil { + t.Errorf("Expected an error but succeeded validation for test case %s", currentTestVector) + } + } +} + +func TestValidateAmountFormat(t *testing.T) { + validCases := []string{ + "0", + "1", + "1.0", + "0.1", + "0.12345678", + "111111111111.11111111", // 12 digits to the left of decimal, 8 digits to the right + "184467440737.09551615", // Maximum input that can be represented in sompi later + "184467440737.09551616", // Cannot be represented in sompi, but we'll acccept for "correct format" + "999999999999.99999999", // Cannot be represented in sompi, but we'll acccept for "correct format" + } + + for _, testCase := range validCases { + err := validateKASAmountFormat(testCase) + + if err != nil { + t.Error(err) + } + } + + invalidCases := []string{ + "", + "a", + "-1", + "0.123456789", // 9 decimal digits + ".1", // decimal but no integer component + "0a", // Extra character + "0000000000000", // 13 zeros + "012", // Int padded with zero + "00.1", // Decimal padded with zeros + "111111111111111111111", // all digits + "111111111111A11111111", // non-period/non-digit where decimal would be + "000000000000.00000000", // all zeros + "kaspa", // all text + } + + for _, testCase := range invalidCases { + err := validateKASAmountFormat(testCase) + + if err == nil { + t.Errorf("Expected an error but succeeded validation for test case %s", testCase) + } + } +} diff --git a/domain/consensus/model/testapi/test_consensus.go b/domain/consensus/model/testapi/test_consensus.go index f7be59406..6738a916c 100644 --- a/domain/consensus/model/testapi/test_consensus.go +++ b/domain/consensus/model/testapi/test_consensus.go @@ -43,6 +43,9 @@ type TestConsensus interface { AddBlock(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) + AddBlockOnTips(coinbaseData *externalapi.DomainCoinbaseData, + transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) + AddUTXOInvalidHeader(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) AddUTXOInvalidBlock(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash, diff --git a/domain/consensus/test_consensus.go b/domain/consensus/test_consensus.go index 6f67c47a3..64d61afdf 100644 --- a/domain/consensus/test_consensus.go +++ b/domain/consensus/test_consensus.go @@ -69,6 +69,17 @@ func (tc *testConsensus) AddBlock(parentHashes []*externalapi.DomainHash, coinba return consensushashing.BlockHash(block), virtualChangeSet, nil } +func (tc *testConsensus) AddBlockOnTips(coinbaseData *externalapi.DomainCoinbaseData, + transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) { + + tips, err := tc.Tips() + if err != nil { + return nil, nil, err + } + + return tc.AddBlock(tips, coinbaseData, transactions) +} + func (tc *testConsensus) AddUTXOInvalidHeader(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) { diff --git a/domain/dagconfig/params.go b/domain/dagconfig/params.go index 0340763ef..7c735924f 100644 --- a/domain/dagconfig/params.go +++ b/domain/dagconfig/params.go @@ -232,6 +232,8 @@ var MainnetParams = Params{ "seeder4.kaspad.net", // This DNS seeder is run by Tim "kaspadns.kaspacalc.net", + // This DNS seeder is run by supertypo + "n-mainnet.kaspa.ws", }, // DAG parameters diff --git a/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go b/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go index 564542db8..76e61101f 100644 --- a/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go +++ b/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go @@ -147,16 +147,12 @@ func (btb *blockTemplateBuilder) BuildBlockTemplate( invalidTxsErr := ruleerrors.ErrInvalidTransactionsInNewBlock{} if errors.As(err, &invalidTxsErr) { log.Criticalf("consensusReference.Consensus().BuildBlock returned invalid txs in BuildBlockTemplate") - invalidTxs := make([]*consensusexternalapi.DomainTransaction, 0, len(invalidTxsErr.InvalidTransactions)) - for _, tx := range invalidTxsErr.InvalidTransactions { - invalidTxs = append(invalidTxs, tx.Transaction) - } - err = btb.mempool.RemoveTransactions(invalidTxs, true) + err = btb.mempool.RemoveInvalidTransactions(&invalidTxsErr) if err != nil { - // mempool.RemoveTransactions might return errors in situations that are perfectly fine in this context. + // mempool.RemoveInvalidTransactions might return errors in situations that are perfectly fine in this context. // TODO: Once the mempool invariants are clear, this should be converted back `return nil, err`: // https://github.com/kaspanet/kaspad/issues/1553 - log.Criticalf("Error from mempool.RemoveTransactions: %+v", err) + log.Criticalf("Error from mempool.RemoveInvalidTransactions: %+v", err) } // We can call this recursively without worry because this should almost never happen return btb.BuildBlockTemplate(coinbaseData) diff --git a/domain/miningmanager/mempool/error.go b/domain/miningmanager/mempool/error.go index d698217d7..827fa9014 100644 --- a/domain/miningmanager/mempool/error.go +++ b/domain/miningmanager/mempool/error.go @@ -51,6 +51,7 @@ const ( RejectDifficulty RejectCode = 0x44 RejectImmatureSpend RejectCode = 0x45 RejectBadOrphan RejectCode = 0x64 + RejectSpamTx RejectCode = 0x65 ) // Map of reject codes back strings for pretty printing. diff --git a/domain/miningmanager/mempool/mempool.go b/domain/miningmanager/mempool/mempool.go index f4d6ef7bf..e511eb83b 100644 --- a/domain/miningmanager/mempool/mempool.go +++ b/domain/miningmanager/mempool/mempool.go @@ -1,6 +1,9 @@ package mempool import ( + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" "sync" "github.com/kaspanet/kaspad/domain/consensusreference" @@ -141,7 +144,57 @@ func (mp *mempool) BlockCandidateTransactions() []*externalapi.DomainTransaction mp.mtx.RLock() defer mp.mtx.RUnlock() - return mp.transactionsPool.allReadyTransactions() + readyTxs := mp.transactionsPool.allReadyTransactions() + var candidateTxs []*externalapi.DomainTransaction + var spamTx *externalapi.DomainTransaction + var spamTxNewestUTXODaaScore uint64 + for _, tx := range readyTxs { + if len(tx.Outputs) > len(tx.Inputs) { + hasCoinbaseInput := false + for _, input := range tx.Inputs { + if input.UTXOEntry.IsCoinbase() { + hasCoinbaseInput = true + break + } + } + + numExtraOuts := len(tx.Outputs) - len(tx.Inputs) + if !hasCoinbaseInput && numExtraOuts > 2 && tx.Fee < uint64(numExtraOuts)*constants.SompiPerKaspa { + log.Debugf("Filtered spam tx %s", consensushashing.TransactionID(tx)) + continue + } + + if hasCoinbaseInput || tx.Fee > uint64(numExtraOuts)*constants.SompiPerKaspa { + candidateTxs = append(candidateTxs, tx) + } else { + txNewestUTXODaaScore := tx.Inputs[0].UTXOEntry.BlockDAAScore() + for _, input := range tx.Inputs { + if input.UTXOEntry.BlockDAAScore() > txNewestUTXODaaScore { + txNewestUTXODaaScore = input.UTXOEntry.BlockDAAScore() + } + } + + if spamTx != nil { + if txNewestUTXODaaScore < spamTxNewestUTXODaaScore { + spamTx = tx + spamTxNewestUTXODaaScore = txNewestUTXODaaScore + } + } else { + spamTx = tx + spamTxNewestUTXODaaScore = txNewestUTXODaaScore + } + } + } else { + candidateTxs = append(candidateTxs, tx) + } + } + + if spamTx != nil { + log.Debugf("Adding spam tx candidate %s", consensushashing.TransactionID(spamTx)) + candidateTxs = append(candidateTxs, spamTx) + } + + return candidateTxs } func (mp *mempool) RevalidateHighPriorityTransactions() (validTransactions []*externalapi.DomainTransaction, err error) { @@ -151,11 +204,29 @@ func (mp *mempool) RevalidateHighPriorityTransactions() (validTransactions []*ex return mp.revalidateHighPriorityTransactions() } -func (mp *mempool) RemoveTransactions(transactions []*externalapi.DomainTransaction, removeRedeemers bool) error { +func (mp *mempool) RemoveInvalidTransactions(err *ruleerrors.ErrInvalidTransactionsInNewBlock) error { mp.mtx.Lock() defer mp.mtx.Unlock() - return mp.removeTransactions(transactions, removeRedeemers) + for _, tx := range err.InvalidTransactions { + ruleErr, success := tx.Error.(ruleerrors.RuleError) + if !success { + continue + } + + inner := ruleErr.Unwrap() + removeRedeemers := true + if _, ok := inner.(ruleerrors.ErrMissingTxOut); ok { + removeRedeemers = false + } + + err := mp.removeTransaction(consensushashing.TransactionID(tx.Transaction), removeRedeemers) + if err != nil { + return err + } + } + + return nil } func (mp *mempool) RemoveTransaction(transactionID *externalapi.DomainTransactionID, removeRedeemers bool) error { diff --git a/domain/miningmanager/mempool/remove_transaction.go b/domain/miningmanager/mempool/remove_transaction.go index da812d872..a21c4a162 100644 --- a/domain/miningmanager/mempool/remove_transaction.go +++ b/domain/miningmanager/mempool/remove_transaction.go @@ -2,20 +2,9 @@ package mempool import ( "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" - "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "github.com/kaspanet/kaspad/domain/miningmanager/mempool/model" ) -func (mp *mempool) removeTransactions(transactions []*externalapi.DomainTransaction, removeRedeemers bool) error { - for _, transaction := range transactions { - err := mp.removeTransaction(consensushashing.TransactionID(transaction), removeRedeemers) - if err != nil { - return err - } - } - return nil -} - func (mp *mempool) removeTransaction(transactionID *externalapi.DomainTransactionID, removeRedeemers bool) error { if _, ok := mp.orphansPool.allOrphans[*transactionID]; ok { return mp.orphansPool.removeOrphan(transactionID, true) diff --git a/domain/miningmanager/mempool/revalidate_high_priority_transactions.go b/domain/miningmanager/mempool/revalidate_high_priority_transactions.go index 12f76403f..bba68388b 100644 --- a/domain/miningmanager/mempool/revalidate_high_priority_transactions.go +++ b/domain/miningmanager/mempool/revalidate_high_priority_transactions.go @@ -7,20 +7,85 @@ import ( ) func (mp *mempool) revalidateHighPriorityTransactions() ([]*externalapi.DomainTransaction, error) { + type txNode struct { + children map[externalapi.DomainTransactionID]struct{} + nonVisitedParents int + tx *model.MempoolTransaction + visited bool + } + onEnd := logger.LogAndMeasureExecutionTime(log, "revalidateHighPriorityTransactions") defer onEnd() + // We revalidate transactions in topological order in case there are dependencies between them + + // Naturally transactions point to their dependencies, but since we want to start processing the dependencies + // first, we build the opposite DAG. We initially fill `queue` with transactions with no dependencies. + txDAG := make(map[externalapi.DomainTransactionID]*txNode) + + maybeAddNode := func(txID externalapi.DomainTransactionID) *txNode { + if node, ok := txDAG[txID]; ok { + return node + } + + node := &txNode{ + children: make(map[externalapi.DomainTransactionID]struct{}), + nonVisitedParents: 0, + tx: mp.transactionsPool.highPriorityTransactions[txID], + } + txDAG[txID] = node + return node + } + + queue := make([]*txNode, 0, len(mp.transactionsPool.highPriorityTransactions)) + for id, transaction := range mp.transactionsPool.highPriorityTransactions { + node := maybeAddNode(id) + + parents := make(map[externalapi.DomainTransactionID]struct{}) + for _, input := range transaction.Transaction().Inputs { + if _, ok := mp.transactionsPool.highPriorityTransactions[input.PreviousOutpoint.TransactionID]; !ok { + continue + } + + parents[input.PreviousOutpoint.TransactionID] = struct{}{} // To avoid duplicate parents, we first add it to a set and then count it + maybeAddNode(input.PreviousOutpoint.TransactionID).children[id] = struct{}{} + } + node.nonVisitedParents = len(parents) + + if node.nonVisitedParents == 0 { + queue = append(queue, node) + } + } + validTransactions := []*externalapi.DomainTransaction{} - for _, transaction := range mp.transactionsPool.highPriorityTransactions { + + // Now we iterate the DAG in topological order using BFS + for len(queue) > 0 { + var node *txNode + node, queue = queue[0], queue[1:] + + if node.visited { + continue + } + node.visited = true + + transaction := node.tx isValid, err := mp.revalidateTransaction(transaction) if err != nil { return nil, err } - if !isValid { - continue + + for child := range node.children { + childNode := txDAG[child] + childNode.nonVisitedParents-- + if childNode.nonVisitedParents == 0 { + queue = append(queue, txDAG[child]) + } } - validTransactions = append(validTransactions, transaction.Transaction().Clone()) + if isValid { + validTransactions = append(validTransactions, transaction.Transaction().Clone()) + } } return validTransactions, nil @@ -35,7 +100,7 @@ func (mp *mempool) revalidateTransaction(transaction *model.MempoolTransaction) } if len(missingParents) > 0 { log.Debugf("Removing transaction %s, it failed revalidation", transaction.TransactionID()) - err := mp.removeTransaction(transaction.TransactionID(), true) + err := mp.removeTransaction(transaction.TransactionID(), false) if err != nil { return false, err } diff --git a/domain/miningmanager/mempool/validate_and_insert_transaction.go b/domain/miningmanager/mempool/validate_and_insert_transaction.go index d06a19cea..ff8c752d3 100644 --- a/domain/miningmanager/mempool/validate_and_insert_transaction.go +++ b/domain/miningmanager/mempool/validate_and_insert_transaction.go @@ -2,7 +2,6 @@ package mempool import ( "fmt" - "github.com/kaspanet/kaspad/infrastructure/logger" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" diff --git a/domain/miningmanager/mempool/validate_transaction.go b/domain/miningmanager/mempool/validate_transaction.go index 42e77c9e2..86695f051 100644 --- a/domain/miningmanager/mempool/validate_transaction.go +++ b/domain/miningmanager/mempool/validate_transaction.go @@ -2,6 +2,7 @@ package mempool import ( "fmt" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" @@ -44,6 +45,20 @@ func (mp *mempool) validateTransactionInIsolation(transaction *externalapi.Domai } func (mp *mempool) validateTransactionInContext(transaction *externalapi.DomainTransaction) error { + hasCoinbaseInput := false + for _, input := range transaction.Inputs { + if input.UTXOEntry.IsCoinbase() { + hasCoinbaseInput = true + break + } + } + + numExtraOuts := len(transaction.Outputs) - len(transaction.Inputs) + if !hasCoinbaseInput && numExtraOuts > 2 && transaction.Fee < uint64(numExtraOuts)*constants.SompiPerKaspa { + log.Warnf("Rejected spam tx %s from mempool (%d outputs)", consensushashing.TransactionID(transaction), len(transaction.Outputs)) + return transactionRuleError(RejectSpamTx, fmt.Sprintf("Rejected spam tx %s from mempool", consensushashing.TransactionID(transaction))) + } + if !mp.config.AcceptNonStandard { err := mp.checkTransactionStandardInContext(transaction) if err != nil { diff --git a/domain/miningmanager/miningmanager_test.go b/domain/miningmanager/miningmanager_test.go index d1b1b8e2e..56cd8be41 100644 --- a/domain/miningmanager/miningmanager_test.go +++ b/domain/miningmanager/miningmanager_test.go @@ -577,6 +577,72 @@ func TestRevalidateHighPriorityTransactions(t *testing.T) { }) } +func TestRevalidateHighPriorityTransactionsWithChain(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, "TestRevalidateHighPriorityTransactions") + if err != nil { + t.Fatalf("Failed setting up TestConsensus: %+v", err) + } + defer teardown(false) + + miningFactory := miningmanager.NewFactory() + mempoolConfig := mempool.DefaultConfig(&consensusConfig.Params) + tcAsConsensus := tc.(externalapi.Consensus) + tcAsConsensusPointer := &tcAsConsensus + consensusReference := consensusreference.NewConsensusReference(&tcAsConsensusPointer) + miningManager := miningFactory.NewMiningManager(consensusReference, &consensusConfig.Params, mempoolConfig) + + const chainSize = 10 + chain, err := createTxChain(tc, chainSize) + if err != nil { + t.Fatal(err) + } + + _, err = miningManager.ValidateAndInsertTransaction(chain[0], true, false) + if err != nil { + t.Fatal(err) + } + + blockHash, _, err := tc.AddBlockOnTips(nil, []*externalapi.DomainTransaction{chain[0].Clone()}) + if err != nil { + t.Fatal(err) + } + + block, _, err := tc.GetBlock(blockHash) + if err != nil { + t.Fatal(err) + } + + _, err = miningManager.HandleNewBlockTransactions(block.Transactions) + if err != nil { + t.Fatal(err) + } + + for _, transaction := range chain[1:] { + _, err = miningManager.ValidateAndInsertTransaction(transaction, true, false) + if err != nil { + t.Fatal(err) + } + } + + _, _, err = tc.AddBlockOnTips(nil, []*externalapi.DomainTransaction{chain[1].Clone()}) + if err != nil { + t.Fatal(err) + } + + revalidated, err := miningManager.RevalidateHighPriorityTransactions() + if err != nil { + t.Fatal(err) + } + + if len(revalidated) != chainSize-2 { + t.Fatalf("expected %d transactions to revalidate but instead only %d revalidated", chainSize-2, len(revalidated)) + } + }) +} + // TestModifyBlockTemplate verifies that modifying a block template changes coinbase data correctly. func TestModifyBlockTemplate(t *testing.T) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { @@ -904,40 +970,58 @@ func createArraysOfParentAndChildrenTransactions(tc testapi.TestConsensus) ([]*e func createParentAndChildrenTransactions(tc testapi.TestConsensus) (txParent *externalapi.DomainTransaction, txChild *externalapi.DomainTransaction, err error) { + chain, err := createTxChain(tc, 2) + if err != nil { + return nil, nil, err + } + + return chain[0], chain[1], nil +} + +func createTxChain(tc testapi.TestConsensus, numTxs int) ([]*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 + return nil, err } _, _, err = tc.AddBlock(tips, nil, nil) if err != nil { - return nil, nil, errors.Wrapf(err, "AddBlock: %v", err) + return nil, errors.Wrapf(err, "AddBlock: %v", err) } tips, err = tc.Tips() if err != nil { - return nil, nil, err + return nil, err } fundingBlockHashForParent, _, err := tc.AddBlock(tips, nil, nil) if err != nil { - return nil, nil, errors.Wrap(err, "AddBlock: ") + return nil, errors.Wrap(err, "AddBlock: ") } fundingBlockForParent, _, err := tc.GetBlock(fundingBlockHashForParent) if err != nil { - return nil, nil, errors.Wrap(err, "GetBlock: ") + return nil, errors.Wrap(err, "GetBlock: ") } fundingTransactionForParent := fundingBlockForParent.Transactions[transactionhelper.CoinbaseTransactionIndex] - txParent, err = testutils.CreateTransaction(fundingTransactionForParent, 1000) + + transactions := make([]*externalapi.DomainTransaction, numTxs) + transactions[0], err = testutils.CreateTransaction(fundingTransactionForParent, 1000) if err != nil { - return nil, nil, err + return nil, err } - txChild, err = testutils.CreateTransaction(txParent, 1000) - if err != nil { - return nil, nil, err + + txParent := transactions[0] + for i := 1; i < numTxs; i++ { + transactions[i], err = testutils.CreateTransaction(txParent, 1000) + if err != nil { + return nil, err + } + + txParent = transactions[i] } - return txParent, txChild, nil + + return transactions, nil } func createChildAndParentTxsAndAddParentToConsensus(tc testapi.TestConsensus) (*externalapi.DomainTransaction, error) { diff --git a/domain/miningmanager/model/interface_mempool.go b/domain/miningmanager/model/interface_mempool.go index fe9be9b5c..777d23cde 100644 --- a/domain/miningmanager/model/interface_mempool.go +++ b/domain/miningmanager/model/interface_mempool.go @@ -2,6 +2,7 @@ package model import ( "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" ) // Mempool maintains a set of known transactions that @@ -11,7 +12,7 @@ type Mempool interface { BlockCandidateTransactions() []*externalapi.DomainTransaction ValidateAndInsertTransaction(transaction *externalapi.DomainTransaction, isHighPriority bool, allowOrphan bool) ( acceptedTransactions []*externalapi.DomainTransaction, err error) - RemoveTransactions(txs []*externalapi.DomainTransaction, removeRedeemers bool) error + RemoveInvalidTransactions(err *ruleerrors.ErrInvalidTransactionsInNewBlock) error GetTransaction( transactionID *externalapi.DomainTransactionID, includeTransactionPool bool, diff --git a/domain/utxoindex/utxoindex.go b/domain/utxoindex/utxoindex.go index a5c4353ad..670fb25b0 100644 --- a/domain/utxoindex/utxoindex.go +++ b/domain/utxoindex/utxoindex.go @@ -52,6 +52,8 @@ func (ui *UTXOIndex) Reset() error { ui.mutex.Lock() defer ui.mutex.Unlock() + log.Infof("Starting UTXO index reset") + err := ui.store.deleteAll() if err != nil { return err @@ -88,7 +90,13 @@ func (ui *UTXOIndex) Reset() error { } // This has to be done last to mark that the reset went smoothly and no reset has to be called next time. - return ui.store.updateAndCommitVirtualParentsWithoutTransaction(virtualInfo.ParentHashes) + err = ui.store.updateAndCommitVirtualParentsWithoutTransaction(virtualInfo.ParentHashes) + if err != nil { + return err + } + + log.Infof("Finished UTXO index reset") + return nil } func (ui *UTXOIndex) isSynced() (bool, error) { diff --git a/version/version.go b/version/version.go index c04cd9643..8224f7faf 100644 --- a/version/version.go +++ b/version/version.go @@ -11,7 +11,7 @@ const validCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs const ( appMajor uint = 0 appMinor uint = 12 - appPatch uint = 13 + appPatch uint = 15 ) // appBuild is defined as a variable so it can be overridden during the build