mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-11-27 07:48:44 +00:00
Merge branch 'dev' into testnet11
This commit is contained in:
commit
a216bd5468
2
.github/workflows/deploy.yaml
vendored
2
.github/workflows/deploy.yaml
vendored
@ -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'
|
||||
|
||||
@ -218,7 +218,7 @@ func RPCTransactionToDomainTransaction(rpcTransaction *RPCTransaction) (*externa
|
||||
Outputs: outputs,
|
||||
LockTime: rpcTransaction.LockTime,
|
||||
SubnetworkID: *subnetworkID,
|
||||
Gas: rpcTransaction.LockTime,
|
||||
Gas: rpcTransaction.Gas,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
@ -286,7 +286,7 @@ func DomainTransactionToRPCTransaction(transaction *externalapi.DomainTransactio
|
||||
Outputs: outputs,
|
||||
LockTime: transaction.LockTime,
|
||||
SubnetworkID: subnetworkID,
|
||||
Gas: transaction.LockTime,
|
||||
Gas: transaction.Gas,
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 :=
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
90
cmd/kaspawallet/utils/format_kas_test.go
Normal file
90
cmd/kaspawallet/utils/format_kas_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
|
||||
@ -387,7 +387,7 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pops) != 20 {
|
||||
if len(pops) != 19 {
|
||||
return nil, nil
|
||||
}
|
||||
isAtomicSwap := pops[0].opcode.value == OpIf &&
|
||||
@ -403,13 +403,12 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
pops[10].opcode.value == OpElse &&
|
||||
canonicalPush(pops[11]) &&
|
||||
pops[12].opcode.value == OpCheckLockTimeVerify &&
|
||||
pops[13].opcode.value == OpDrop &&
|
||||
pops[14].opcode.value == OpDup &&
|
||||
pops[15].opcode.value == OpBlake2b &&
|
||||
pops[16].opcode.value == OpData32 &&
|
||||
pops[17].opcode.value == OpEndIf &&
|
||||
pops[18].opcode.value == OpEqualVerify &&
|
||||
pops[19].opcode.value == OpCheckSig
|
||||
pops[13].opcode.value == OpDup &&
|
||||
pops[14].opcode.value == OpBlake2b &&
|
||||
pops[15].opcode.value == OpData32 &&
|
||||
pops[16].opcode.value == OpEndIf &&
|
||||
pops[17].opcode.value == OpEqualVerify &&
|
||||
pops[18].opcode.value == OpCheckSig
|
||||
if !isAtomicSwap {
|
||||
return nil, nil
|
||||
}
|
||||
@ -417,9 +416,9 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
pushes := new(AtomicSwapDataPushes)
|
||||
copy(pushes.SecretHash[:], pops[5].data)
|
||||
copy(pushes.RecipientBlake2b[:], pops[9].data)
|
||||
copy(pushes.RefundBlake2b[:], pops[16].data)
|
||||
copy(pushes.RefundBlake2b[:], pops[15].data)
|
||||
if pops[2].data != nil {
|
||||
locktime, err := makeScriptNum(pops[2].data, 5)
|
||||
locktime, err := makeScriptNum(pops[2].data, 8)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@ -430,7 +429,7 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
return nil, nil
|
||||
}
|
||||
if pops[11].data != nil {
|
||||
locktime, err := makeScriptNum(pops[11].data, 5)
|
||||
locktime, err := makeScriptNum(pops[11].data, 8)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -7,21 +7,86 @@ 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])
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package mempool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kaspanet/kaspad/infrastructure/logger"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
txParent := transactions[0]
|
||||
for i := 1; i < numTxs; i++ {
|
||||
transactions[i], err = testutils.CreateTransaction(txParent, 1000)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
return txParent, txChild, nil
|
||||
|
||||
txParent = transactions[i]
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func createChildAndParentTxsAndAddParentToConsensus(tc testapi.TestConsensus) (*externalapi.DomainTransaction, error) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user