Implement a stability test for mempool limits (#1647)

* Copy some boilerplate from the other stability tests.

* Fix a copy+paste error in run.sh.

* Copy over some stability test boilerplate go code.

* Run kaspad in the background.

* Catch panics and initialize the RPC client.

* Mine enough blocks to fund filling up the mempool.

* Extract coinbase transactions out of the generated blocks.

* Tidy up a bit.

* Implement submitting transactions.

* Lower the amount of outputs in each transaction.

* Verify that the mempool size has the expected amount of transactions.

* Pregenerate enough funds before submitting the first transaction so that block creation doesn't interfere with the test.

* Empty mempool out by continuously adding blocks to the DAG.

* Handle orphan transactions when overfilling the mempool.

* Increase mempoolSizeLimit to 1m.

* Fix a comment.

* Fix a comment.

* Add mempool-limits to run-slow.sh.

* Rename generateTransactionsWithLotsOfOutputs to generateTransactionsWithMultipleOutputs.

* Rename generateCoinbaseTransaction to mineBlockAndGetCoinbaseTransaction.

* Make generateFundingCoinbaseTransactions return an object instead of store a global variable.

* Convert mempool-limits into a Go test.

* Convert panics to t.Fatalfs.

* Fix a comment.

* Increase mempoolSizeLimit to 1m.

* Run TestMempoolLimits only if RUN_STABILITY_TESTS is set.

* Move the run of mempool-limits in run-slow.sh.

* Add a comment above fundingCoinbaseTransactions.

* Make a couple of stylistic changes.

* Use transactionhelper.CoinbaseTransactionIndex instead of hardcoding 0.

* Make uninteresting errors print %+v instead of %s.

Co-authored-by: Svarog <feanorr@gmail.com>
This commit is contained in:
stasatdaglabs 2021-04-11 16:59:11 +03:00 committed by GitHub
parent 3c3ad1425d
commit dfa24d8353
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 454 additions and 2 deletions

View File

@ -11,8 +11,8 @@ func (msg *GetInfoRequestMessage) Command() MessageCommand {
return CmdGetInfoRequestMessage
}
// NewGeInfoRequestMessage returns a instance of the message
func NewGeInfoRequestMessage() *GetInfoRequestMessage {
// NewGetInfoRequestMessage returns a instance of the message
func NewGetInfoRequestMessage() *GetInfoRequestMessage {
return &GetInfoRequestMessage{}
}

View File

@ -0,0 +1,20 @@
package rpcclient
import "github.com/kaspanet/kaspad/app/appmessage"
// GetInfo sends an RPC request respective to the function's name and returns the RPC server's response
func (c *RPCClient) GetInfo() (*appmessage.GetInfoResponseMessage, error) {
err := c.rpcRouter.outgoingRoute().Enqueue(appmessage.NewGetInfoRequestMessage())
if err != nil {
return nil, err
}
response, err := c.route(appmessage.CmdGetInfoResponseMessage).DequeueWithTimeout(c.timeout)
if err != nil {
return nil, err
}
getInfoResponse := response.(*appmessage.GetInfoResponseMessage)
if getInfoResponse.Error != nil {
return nil, c.convertRPCError(getInfoResponse.Error)
}
return getInfoResponse, nil
}

View File

@ -0,0 +1,14 @@
# Mempool Limits tool
This tool:
1. Fills up the mempool beyond its transaction limit to make sure eviction works correctly
2. Mines blocks until the mempool is expected to become empty
## Running
1. `go install` kaspad and mempool-limits.
2. `cd run`
3. `./run.sh`

View File

@ -0,0 +1,44 @@
package main
import (
"path/filepath"
"github.com/jessevdk/go-flags"
"github.com/kaspanet/kaspad/stability-tests/common"
)
const (
defaultLogFilename = "mempool-limits.log"
defaultErrLogFilename = "mempool-limits_err.log"
)
var (
// Default configuration options
defaultLogFile = filepath.Join(common.DefaultAppDir, defaultLogFilename)
defaultErrLogFile = filepath.Join(common.DefaultAppDir, defaultErrLogFilename)
)
type configFlags struct {
LogLevel string `long:"loglevel" description:"Set log level {trace, debug, info, warn, error, critical}"`
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
KaspadRPCAddress string `long:"rpc-address" description:"RPC address of the kaspad node"`
}
var cfg *configFlags
func activeConfig() *configFlags {
return cfg
}
func parseConfig() error {
cfg = &configFlags{}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag|flags.IgnoreUnknown)
_, err := parser.Parse()
if err != nil {
return err
}
initLog(defaultLogFile, defaultErrLogFile)
return nil
}

View File

@ -0,0 +1,30 @@
package main
import (
"fmt"
"os"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/stability-tests/common"
"github.com/kaspanet/kaspad/util/panics"
)
var (
backendLog = logger.NewBackend()
log = backendLog.Logger("MPLM")
spawn = panics.GoroutineWrapperFunc(log)
)
func initLog(logFile, errLogFile string) {
level := logger.LevelInfo
if activeConfig().LogLevel != "" {
var ok bool
level, ok = logger.LevelFromString(activeConfig().LogLevel)
if !ok {
fmt.Fprintf(os.Stderr, "Log level %s doesn't exists", activeConfig().LogLevel)
os.Exit(1)
}
}
log.SetLevel(level)
common.InitBackend(backendLog, logFile, errLogFile)
}

View File

@ -0,0 +1,111 @@
package main
import (
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/kaspanet/kaspad/stability-tests/common"
"github.com/kaspanet/kaspad/util/panics"
"github.com/kaspanet/kaspad/util/profiling"
"os"
"testing"
)
const (
mempoolSizeLimit = 1_000_000
overfillMempoolByAmount = 1_000
)
func TestMempoolLimits(t *testing.T) {
if os.Getenv("RUN_STABILITY_TESTS") == "" {
t.Skip()
}
defer panics.HandlePanic(log, "mempool-limits-main", nil)
err := parseConfig()
if err != nil {
t.Fatalf("error in parseConfig: %s", err)
}
defer backendLog.Close()
common.UseLogger(backendLog, log.Level())
cfg := activeConfig()
if cfg.Profile != "" {
profiling.Start(cfg.Profile, log)
}
payAddressKeyPair := decodePayAddressKeyPair(t)
payToPayAddressScript := buildPayToPayAddressScript(t)
rpcClient := buildRPCClient(t)
// Create enough funds for the test
fundingTransactions := generateFundingCoinbaseTransactions(t, rpcClient)
// Fill up the mempool to the brim
submitAnAmountOfTransactionsToTheMempool(t, rpcClient, payAddressKeyPair,
payToPayAddressScript, fundingTransactions, mempoolSizeLimit, false)
// Make sure that the mempool size is exactly the limit
mempoolSize := getMempoolSize(t, rpcClient)
if mempoolSize != mempoolSizeLimit {
t.Fatalf("Unexpected mempool size. Want: %d, got: %d",
mempoolSizeLimit, mempoolSize)
}
// Add some more transactions to the mempool. We expect the
// mempool to either not grow or even to shrink, since an eviction
// may also remove any dependant (chained) transactions.
// Note that we pass ignoreOrphanRejects: true because we
// expect some of the submitted transactions to depend on
// transactions that had been evicted from the mempool
submitAnAmountOfTransactionsToTheMempool(t, rpcClient, payAddressKeyPair,
payToPayAddressScript, fundingTransactions, overfillMempoolByAmount, true)
// Make sure that the mempool size is the limit or smaller
mempoolSize = getMempoolSize(t, rpcClient)
if mempoolSize > mempoolSizeLimit {
t.Fatalf("Unexpected mempool size. Want at most: %d, got: %d",
mempoolSizeLimit, mempoolSize)
}
// Empty mempool out by continuously adding blocks to the DAG
emptyOutMempool(t, rpcClient)
log.Infof("mempool-limits passed")
}
func buildRPCClient(t *testing.T) *rpcclient.RPCClient {
client, err := rpcclient.NewRPCClient(activeConfig().KaspadRPCAddress)
if err != nil {
t.Fatalf("error connecting to %s: %s", activeConfig().KaspadRPCAddress, err)
}
return client
}
func getMempoolSize(t *testing.T, rpcClient *rpcclient.RPCClient) uint64 {
getInfoResponse, err := rpcClient.GetInfo()
if err != nil {
t.Fatalf("GetInfo: %+v", err)
}
return getInfoResponse.MempoolSize
}
func emptyOutMempool(t *testing.T, rpcClient *rpcclient.RPCClient) {
log.Infof("Adding blocks until mempool shrinks to 0 transactions")
getInfoResponse, err := rpcClient.GetInfo()
if err != nil {
t.Fatalf("GetInfo: %+v", err)
}
currentMempoolSize := getInfoResponse.MempoolSize
for currentMempoolSize > 0 {
mineBlockAndGetCoinbaseTransaction(t, rpcClient)
getInfoResponse, err := rpcClient.GetInfo()
if err != nil {
t.Fatalf("GetInfo: %+v", err)
}
if getInfoResponse.MempoolSize == currentMempoolSize {
t.Fatalf("Mempool did not shrink after a block was added to the DAG")
}
log.Infof("Mempool shrank from %d transactions to %d transactions",
currentMempoolSize, getInfoResponse.MempoolSize)
currentMempoolSize = getInfoResponse.MempoolSize
}
}

View File

@ -0,0 +1,29 @@
#!/bin/bash
APPDIR=/tmp/kaspad-temp
KASPAD_RPC_PORT=29587
rm -rf "${APPDIR}"
kaspad --simnet --appdir="${APPDIR}" --rpclisten=0.0.0.0:"${KASPAD_RPC_PORT}" --profile=6061 &
KASPAD_PID=$!
sleep 1
RUN_STABILITY_TESTS=true go test ../ -- --rpc-address=127.0.0.1:"${KASPAD_RPC_PORT}" --profile=7000
TEST_EXIT_CODE=$?
kill $KASPAD_PID
wait $KASPAD_PID
KASPAD_EXIT_CODE=$?
echo "Exit code: $TEST_EXIT_CODE"
echo "Kaspad exit code: $KASPAD_EXIT_CODE"
if [ $TEST_EXIT_CODE -eq 0 ] && [ $KASPAD_EXIT_CODE -eq 0 ]; then
echo "mempool-limits test: PASSED"
exit 0
fi
echo "mempool-limits test: FAILED"
exit 1

View File

@ -0,0 +1,200 @@
package main
import (
"encoding/hex"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
utxopkg "github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/kaspanet/kaspad/stability-tests/common/mine"
"github.com/kaspanet/kaspad/util"
"strings"
"testing"
)
const (
payAddress = "kaspasim:qr79e37hxdgkn4xjjmfxvqvayc5gsmsql2660d08u9ej9vnc8lzcywr265u64"
payAddressPrivateKey = "0ec5d7308f65717f3f0c3e4d962d73056c1c255a16593b3989589281b51ad5bc"
fundingCoinbaseTransactionAmount = 1000
outputsPerTransaction = 3
transactionFee = 1000
coinbaseMaturity = 100
)
// fundingCoinbaseTransactions contains a collection of transactions
// to be utilized when generating further transactions to fill up
// the mempool.
// It's a separate type because we modify the transactions in place
// whenever we pass an instance of this type into
// submitAnAmountOfTransactionsToTheMempool.
type fundingCoinbaseTransactions struct {
transactions []*externalapi.DomainTransaction
}
func generateFundingCoinbaseTransactions(t *testing.T, rpcClient *rpcclient.RPCClient) *fundingCoinbaseTransactions {
// Mine a block, since we need at least one block above the genesis
// to create a spendable UTXO
mineBlockAndGetCoinbaseTransaction(t, rpcClient)
log.Infof("Generating funding coinbase transactions")
fundingCoinbaseTransactions := &fundingCoinbaseTransactions{
transactions: make([]*externalapi.DomainTransaction, fundingCoinbaseTransactionAmount),
}
for i := 0; i < fundingCoinbaseTransactionAmount; i++ {
fundingCoinbaseTransactions.transactions[i] = mineBlockAndGetCoinbaseTransaction(t, rpcClient)
}
log.Infof("Maturing funding coinbase transactions")
for i := 0; i < coinbaseMaturity; i++ {
mineBlockAndGetCoinbaseTransaction(t, rpcClient)
}
return fundingCoinbaseTransactions
}
func submitAnAmountOfTransactionsToTheMempool(t *testing.T, rpcClient *rpcclient.RPCClient,
payAddressKeyPair *secp256k1.SchnorrKeyPair, payToPayAddressScript *externalapi.ScriptPublicKey,
fundingTransactions *fundingCoinbaseTransactions, amountToSubmit int, ignoreOrphanRejects bool) {
log.Infof("Generating %d transactions", amountToSubmit)
transactions := make([]*externalapi.DomainTransaction, 0)
for len(transactions) < amountToSubmit {
var coinbaseTransaction *externalapi.DomainTransaction
coinbaseTransaction, fundingTransactions.transactions = fundingTransactions.transactions[0], fundingTransactions.transactions[1:]
unspentTransactions := []*externalapi.DomainTransaction{coinbaseTransaction}
for len(transactions) < amountToSubmit && len(unspentTransactions) > 0 {
var transactionToSpend *externalapi.DomainTransaction
transactionToSpend, unspentTransactions = unspentTransactions[0], unspentTransactions[1:]
spendingTransactions := generateTransactionsWithMultipleOutputs(t, payAddressKeyPair, payToPayAddressScript, transactionToSpend)
transactions = append(transactions, spendingTransactions...)
unspentTransactions = append(unspentTransactions, spendingTransactions...)
}
log.Infof("Generated %d transactions", len(transactions))
}
transactions = transactions[:amountToSubmit]
log.Infof("Submitting %d transactions", len(transactions))
for i, transaction := range transactions {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
_, err := rpcClient.SubmitTransaction(rpcTransaction)
if err != nil {
if ignoreOrphanRejects && strings.Contains(err.Error(), "orphan") {
continue
}
t.Fatalf("SubmitTransaction: %+v", err)
}
log.Infof("Submitted %d transactions", i+1)
}
}
func mineBlockAndGetCoinbaseTransaction(t *testing.T, rpcClient *rpcclient.RPCClient) *externalapi.DomainTransaction {
getBlockTemplateResponse, err := rpcClient.GetBlockTemplate(payAddress)
if err != nil {
t.Fatalf("GetBlockTemplate: %+v", err)
}
templateBlock, err := appmessage.RPCBlockToDomainBlock(getBlockTemplateResponse.Block)
if err != nil {
t.Fatalf("RPCBlockToDomainBlock: %+v", err)
}
mine.SolveBlock(templateBlock)
_, err = rpcClient.SubmitBlock(templateBlock)
if err != nil {
t.Fatalf("SubmitBlock: %+v", err)
}
return templateBlock.Transactions[transactionhelper.CoinbaseTransactionIndex]
}
func generateTransactionsWithMultipleOutputs(t *testing.T,
payAddressKeyPair *secp256k1.SchnorrKeyPair, payToPayAddressScript *externalapi.ScriptPublicKey,
fundingTransaction *externalapi.DomainTransaction) []*externalapi.DomainTransaction {
var transactions []*externalapi.DomainTransaction
for fundingTransactionOutputIndex, fundingTransactionOutput := range fundingTransaction.Outputs {
if fundingTransactionOutput.Value < transactionFee {
continue
}
outputValue := (fundingTransactionOutput.Value - transactionFee) / outputsPerTransaction
fundingTransactionID := consensushashing.TransactionID(fundingTransaction)
spendingTransactionInputs := []*externalapi.DomainTransactionInput{
{
PreviousOutpoint: externalapi.DomainOutpoint{
TransactionID: *fundingTransactionID,
Index: uint32(fundingTransactionOutputIndex),
},
UTXOEntry: utxopkg.NewUTXOEntry(
fundingTransactionOutput.Value,
payToPayAddressScript,
false,
0),
},
}
spendingTransactionOutputs := make([]*externalapi.DomainTransactionOutput, outputsPerTransaction)
for i := 0; i < outputsPerTransaction; i++ {
spendingTransactionOutputs[i] = &externalapi.DomainTransactionOutput{
Value: outputValue,
ScriptPublicKey: payToPayAddressScript,
}
}
spendingTransaction := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: spendingTransactionInputs,
Outputs: spendingTransactionOutputs,
LockTime: 0,
SubnetworkID: subnetworks.SubnetworkIDNative,
Gas: 0,
Payload: nil,
}
for spendingTransactionInputIndex, spendingTransactionInput := range spendingTransactionInputs {
signatureScript, err := txscript.SignatureScript(
spendingTransaction,
spendingTransactionInputIndex,
consensushashing.SigHashAll,
payAddressKeyPair,
&consensushashing.SighashReusedValues{})
if err != nil {
t.Fatalf("SignatureScript: %+v", err)
}
spendingTransactionInput.SignatureScript = signatureScript
}
transactions = append(transactions, spendingTransaction)
}
return transactions
}
func decodePayAddressKeyPair(t *testing.T) *secp256k1.SchnorrKeyPair {
privateKeyBytes, err := hex.DecodeString(payAddressPrivateKey)
if err != nil {
t.Fatalf("DecodeString: %+v", err)
}
keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes)
if err != nil {
t.Fatalf("DeserializeSchnorrPrivateKeyFromSlice: %+v", err)
}
return keyPair
}
func buildPayToPayAddressScript(t *testing.T) *externalapi.ScriptPublicKey {
address, err := util.DecodeAddress(payAddress, dagconfig.SimnetParams.Prefix)
if err != nil {
t.Fatalf("DecodeAddress: %+v", err)
}
script, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
return script
}

View File

@ -38,6 +38,10 @@ echo "Running reorg"
cd "${PROJECT_ROOT}/reorg/run" && ./run.sh || failedTests+=("reorg")
echo "Done running reorg"
echo "Running mempool-limits"
cd "${PROJECT_ROOT}/mempool-limits/run" && ./run.sh || failedTests+=("mempool-limits")
echo "Done running mempool-limits"
echo "Running netsync - slow"
cd ${PROJECT_ROOT}/netsync/run"" && ./run.sh || failedTests+=("netsync")
echo "Done running netsync - slow"