diff --git a/.github/demo.gif b/.github/demo.gif index 0c1091b..d333c72 100644 Binary files a/.github/demo.gif and b/.github/demo.gif differ diff --git a/README.md b/README.md index 194aabf..e8f45fb 100644 --- a/README.md +++ b/README.md @@ -54,14 +54,14 @@ USAGE Starts the stress testing suite against a Gno TM2 cluster FLAGS - -batch 20 the batch size of JSON-RPC transactions + -batch 100 the batch size of JSON-RPC transactions -chain-id dev the chain ID of the Gno blockchain - -mnemonic ... the mnemonic used to generate sub-accounts + -mnemonic string the mnemonic used to generate sub-accounts -mode REALM_DEPLOYMENT the mode for the stress test. Possible modes: [REALM_DEPLOYMENT, PACKAGE_DEPLOYMENT, REALM_CALL] - -output ... the output path for the results JSON + -output string the output path for the results JSON -sub-accounts 10 the number of sub-accounts that will send out transactions -transactions 100 the total number of transactions to be emitted - -url ... the JSON-RPC URL of the cluster + -url string the JSON-RPC URL of the cluster ``` ## Modes diff --git a/cmd/root.go b/cmd/root.go index c66d625..335df65 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -93,7 +93,7 @@ func registerFlags(fs *flag.FlagSet, c *internal.Config) { fs.Uint64Var( &c.BatchSize, "batch", - 20, + 100, "the batch size of JSON-RPC transactions", ) } diff --git a/go.mod b/go.mod index 1b792d2..6c5e9c7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gnolang/supernova go 1.21 require ( + github.com/gnolang/gno v0.1.0-nightly.20240604 github.com/peterbourgon/ff/v3 v3.4.0 github.com/schollz/progressbar/v3 v3.14.4 github.com/stretchr/testify v1.9.0 @@ -11,57 +12,53 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect + github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/rs/cors v1.10.1 // indirect - github.com/rs/xid v1.5.0 // indirect - github.com/zondax/hid v0.9.2 // indirect - github.com/zondax/ledger-go v0.14.3 // indirect - go.opentelemetry.io/otel v1.25.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - go.opentelemetry.io/otel/sdk v1.25.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.25.0 // indirect - go.opentelemetry.io/otel/trace v1.25.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect - golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/grpc v1.63.0 // indirect -) - -require ( - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/gnolang/gno v0.0.0-20240509142750-711f4d03a167 github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/websocket v1.5.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/linxGnu/grocksdb v1.7.15 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/onsi/gomega v1.31.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/cors v1.10.1 // indirect + github.com/rs/xid v1.5.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.etcd.io/bbolt v1.3.9 // indirect + go.opentelemetry.io/otel v1.25.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.25.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/otel/sdk v1.25.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.25.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect + google.golang.org/grpc v1.63.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5c6fe7e..9126e09 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/gnolang/gno v0.0.0-20240509142750-711f4d03a167 h1:AW+W/XosfOZnhNZejtipmbPM9OzUwc4naEumnF7Tmzk= -github.com/gnolang/gno v0.0.0-20240509142750-711f4d03a167/go.mod h1:vbm5sS6KZj2rkwJ9C6AtdpPZB7aZDhfZ3lJUC+qa/to= +github.com/gnolang/gno v0.1.0-nightly.20240604 h1:1ennenQkBiXVbypporU1R1v3cWf1T6ol7Ava0rvIAKA= +github.com/gnolang/gno v0.1.0-nightly.20240604/go.mod h1:R9GL0bPvjrcZlWBmmQ5wMssKpBdJK/e5mZdUbkk5sV0= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -84,8 +84,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYp github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= -github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= @@ -97,8 +95,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/linxGnu/grocksdb v1.7.15 h1:AEhP28lkeAybv5UYNYviYISpR6bJejEnKuYbnWAnxx0= -github.com/linxGnu/grocksdb v1.7.15/go.mod h1:pY55D0o+r8yUYLq70QmhdudxYvoDb9F+9puf4m3/W+U= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= @@ -180,6 +176,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/client/batch.go b/internal/client/batch.go new file mode 100644 index 0000000..1c7a382 --- /dev/null +++ b/internal/client/batch.go @@ -0,0 +1,24 @@ +package client + +import ( + "context" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type Batch struct { + batch *client.RPCBatch +} + +func (b *Batch) AddTxBroadcast(tx []byte) error { + if err := b.batch.BroadcastTxSync(tx); err != nil { + return fmt.Errorf("unable to prepare transaction, %w", err) + } + + return nil +} + +func (b *Batch) Execute() ([]interface{}, error) { + return b.batch.Send(context.Background()) +} diff --git a/internal/client/http_client.go b/internal/client/client.go similarity index 66% rename from internal/client/http_client.go rename to internal/client/client.go index 2f5fd67..e8771a6 100644 --- a/internal/client/http_client.go +++ b/internal/client/client.go @@ -1,7 +1,6 @@ package client import ( - "context" "fmt" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -12,47 +11,43 @@ import ( "github.com/gnolang/supernova/internal/common" ) -type Batch struct { - batch *client.RPCBatch +type Client struct { + conn *client.RPCClient } -func (b *Batch) AddTxBroadcast(tx []byte) error { - if err := b.batch.BroadcastTxSync(tx); err != nil { - return fmt.Errorf("unable to prepare transaction, %w", err) +// NewWSClient creates a new instance of the WS client +func NewWSClient(url string) (*Client, error) { + cli, err := client.NewWSClient(url) + if err != nil { + return nil, fmt.Errorf("unable to create ws client, %w", err) } - return nil -} - -func (b *Batch) Execute() ([]interface{}, error) { - return b.batch.Send(context.Background()) -} - -type HTTPClient struct { - conn *client.RPCClient + return &Client{ + conn: cli, + }, nil } // NewHTTPClient creates a new instance of the HTTP client -func NewHTTPClient(url string) (*HTTPClient, error) { +func NewHTTPClient(url string) (*Client, error) { cli, err := client.NewHTTPClient(url) if err != nil { return nil, fmt.Errorf("unable to create http client, %w", err) } - return &HTTPClient{ + return &Client{ conn: cli, }, nil } -func (h *HTTPClient) CreateBatch() common.Batch { +func (h *Client) CreateBatch() common.Batch { return &Batch{batch: h.conn.NewBatch()} } -func (h *HTTPClient) ExecuteABCIQuery(path string, data []byte) (*core_types.ResultABCIQuery, error) { +func (h *Client) ExecuteABCIQuery(path string, data []byte) (*core_types.ResultABCIQuery, error) { return h.conn.ABCIQuery(path, data) } -func (h *HTTPClient) GetLatestBlockHeight() (int64, error) { +func (h *Client) GetLatestBlockHeight() (int64, error) { status, err := h.conn.Status() if err != nil { return 0, fmt.Errorf("unable to fetch status, %w", err) @@ -61,19 +56,19 @@ func (h *HTTPClient) GetLatestBlockHeight() (int64, error) { return status.SyncInfo.LatestBlockHeight, nil } -func (h *HTTPClient) GetBlock(height *int64) (*core_types.ResultBlock, error) { +func (h *Client) GetBlock(height *int64) (*core_types.ResultBlock, error) { return h.conn.Block(height) } -func (h *HTTPClient) GetBlockResults(height *int64) (*core_types.ResultBlockResults, error) { +func (h *Client) GetBlockResults(height *int64) (*core_types.ResultBlockResults, error) { return h.conn.BlockResults(height) } -func (h *HTTPClient) GetConsensusParams(height *int64) (*core_types.ResultConsensusParams, error) { +func (h *Client) GetConsensusParams(height *int64) (*core_types.ResultConsensusParams, error) { return h.conn.ConsensusParams(height) } -func (h *HTTPClient) BroadcastTransaction(tx *std.Tx) error { +func (h *Client) BroadcastTransaction(tx *std.Tx) error { marshalledTx, err := amino.Marshal(tx) if err != nil { return fmt.Errorf("unable to marshal transaction, %w", err) @@ -95,7 +90,7 @@ func (h *HTTPClient) BroadcastTransaction(tx *std.Tx) error { return nil } -func (h *HTTPClient) GetAccount(address string) (*gnoland.GnoAccount, error) { +func (h *Client) GetAccount(address string) (*gnoland.GnoAccount, error) { queryResult, err := h.conn.ABCIQuery( fmt.Sprintf("auth/accounts/%s", address), []byte{}, @@ -117,7 +112,7 @@ func (h *HTTPClient) GetAccount(address string) (*gnoland.GnoAccount, error) { return &acc, nil } -func (h *HTTPClient) GetBlockGasUsed(height int64) (int64, error) { +func (h *Client) GetBlockGasUsed(height int64) (int64, error) { blockRes, err := h.conn.BlockResults(&height) if err != nil { return 0, fmt.Errorf("unable to fetch block results, %w", err) @@ -131,7 +126,7 @@ func (h *HTTPClient) GetBlockGasUsed(height int64) (int64, error) { return gasUsed, nil } -func (h *HTTPClient) GetBlockGasLimit(height int64) (int64, error) { +func (h *Client) GetBlockGasLimit(height int64) (int64, error) { consensusParams, err := h.conn.ConsensusParams(&height) if err != nil { return 0, fmt.Errorf("unable to fetch block info, %w", err) diff --git a/internal/collector/collector.go b/internal/collector/collector.go index 9653cb0..0c257a2 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -3,7 +3,6 @@ package collector import ( "errors" "fmt" - "math" "time" "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -118,7 +117,6 @@ func (c *Collector) GetRunResult( return &RunResult{ AverageTPS: calculateTPS( startTime, - blockResults[len(blockResults)-1].Time, len(txHashes), ), Blocks: blockResults, @@ -160,9 +158,8 @@ func (t *txLookup) anyBelong(txs types.Txs) int { } // calculateTPS calculates the TPS for the sequence -func calculateTPS(startBlock, endBlock time.Time, totalTx int) int { - diff := endBlock.Sub(startBlock).Seconds() +func calculateTPS(startTime time.Time, totalTx int) float64 { + diff := time.Now().Sub(startTime).Seconds() - // ceil(numTxs / commit time) - return int(math.Ceil(float64(totalTx) / diff)) + return float64(totalTx) / diff } diff --git a/internal/collector/collector_test.go b/internal/collector/collector_test.go index 90ebff0..cd1970c 100644 --- a/internal/collector/collector_test.go +++ b/internal/collector/collector_test.go @@ -114,7 +114,7 @@ func TestCollector_GetRunResults(t *testing.T) { t.Fatal("result should not be nil") } - assert.Equal(t, 2, result.AverageTPS) // 1 tx per block; 100 blocks; 1s per block; + assert.NotZero(t, result.AverageTPS) assert.Len(t, result.Blocks, numTxs) for index, block := range result.Blocks { diff --git a/internal/collector/types.go b/internal/collector/types.go index 3b0aecc..da41d2d 100644 --- a/internal/collector/types.go +++ b/internal/collector/types.go @@ -15,7 +15,7 @@ type Client interface { // RunResult is the complete test-run result type RunResult struct { - AverageTPS int `json:"averageTPS"` + AverageTPS float64 `json:"averageTPS"` Blocks []*BlockResult `json:"blocks"` } diff --git a/internal/common/common.go b/internal/common/common.go index 631b33f..83c1324 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -2,11 +2,7 @@ package common import "github.com/gnolang/gno/tm2/pkg/std" -const ( - Denomination = "ugnot" - EncryptPassword = "encrypt" - KeybasePrefix = "stress-account-" -) +const Denomination = "ugnot" // TODO support estimating gas params // These are constants for now, diff --git a/internal/config.go b/internal/config.go index 367ccbb..1423bb6 100644 --- a/internal/config.go +++ b/internal/config.go @@ -18,8 +18,11 @@ var ( ) var ( - // urlRegex is used for verifying the cluster's JSON-RPC endpoint - urlRegex = regexp.MustCompile(`(https?://.*)(:(\d*)\/?(.*))?`) + // httpRegex is used for verifying the cluster's JSON-RPC HTTP endpoint + httpRegex = regexp.MustCompile(`(https?://.*)(:(\d*)\/?(.*))?`) + + // wsRegex is used for verifying the cluster's JSON-RPC WS endpoint + wsRegex = regexp.MustCompile(`(wss?://.*)(:(\d*)\/?(.*))?`) ) // Config is the central pipeline configuration @@ -38,7 +41,8 @@ type Config struct { // Validate validates the stress-test configuration func (cfg *Config) Validate() error { // Make sure the URL is valid - if !urlRegex.MatchString(cfg.URL) { + if !httpRegex.MatchString(cfg.URL) && + !wsRegex.MatchString(cfg.URL) { return errInvalidURL } diff --git a/internal/distributor/distributor.go b/internal/distributor/distributor.go index 77c7cb1..8dca7af 100644 --- a/internal/distributor/distributor.go +++ b/internal/distributor/distributor.go @@ -7,10 +7,10 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/supernova/internal/common" + "github.com/gnolang/supernova/internal/signer" "github.com/schollz/progressbar/v3" ) @@ -23,34 +23,29 @@ type Client interface { BroadcastTransaction(tx *std.Tx) error } -type Signer interface { - SignTx(tx *std.Tx, account *gnoland.GnoAccount, nonce uint64, passphrase string) error -} - // Distributor is the process // that manages sub-account distributions type Distributor struct { - cli Client - signer Signer + cli Client } // NewDistributor creates a new instance of the distributor func NewDistributor( cli Client, - signer Signer, ) *Distributor { return &Distributor{ - cli: cli, - signer: signer, + cli: cli, } } // Distribute distributes the funds from the base account // (account 0 in the mnemonic) to other subaccounts func (d *Distributor) Distribute( - accounts []keys.Info, + distributor crypto.PrivKey, + accounts []crypto.Address, transactions uint64, -) ([]*gnoland.GnoAccount, error) { + chainID string, +) ([]std.Account, error) { fmt.Printf("\n💸 Starting Fund Distribution 💸\n\n") // Calculate the base fees @@ -62,7 +57,7 @@ func (d *Distributor) Distribute( ) // Fund the accounts - return d.fundAccounts(accounts, subAccountCost) + return d.fundAccounts(distributor, accounts, subAccountCost, chainID) } // calculateRuntimeCosts calculates the amount of funds @@ -87,7 +82,12 @@ func calculateRuntimeCosts(totalTx int64) std.Coin { // fundAccounts attempts to fund accounts that have missing funds, // and returns the accounts that can participate in the stress test -func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) ([]*gnoland.GnoAccount, error) { +func (d *Distributor) fundAccounts( + distributorKey crypto.PrivKey, + accounts []crypto.Address, + singleRunCost std.Coin, + chainID string, +) ([]std.Account, error) { type shortAccount struct { address crypto.Address missingFunds std.Coin @@ -95,7 +95,7 @@ func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) var ( // Accounts that are ready (funded) for the run - readyAccounts = make([]*gnoland.GnoAccount, 0, len(accounts)) + readyAccounts = make([]std.Account, 0, len(accounts)) // Accounts that need funding shortAccounts = make([]shortAccount, 0, len(accounts)) @@ -103,9 +103,9 @@ func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) // Check if there are any accounts that need to be funded // before the stress test starts - for _, account := range accounts[1:] { + for _, account := range accounts { // Fetch the account balance - subAccount, err := d.cli.GetAccount(account.GetAddress().String()) + subAccount, err := d.cli.GetAccount(account.String()) if err != nil { return nil, fmt.Errorf("unable to fetch sub-account, %w", err) } @@ -114,7 +114,7 @@ func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) if subAccount.Coins.AmountOf(common.Denomination) < singleRunCost.Amount { // Mark the account as needing a top-up shortAccounts = append(shortAccounts, shortAccount{ - address: account.GetAddress(), + address: account, missingFunds: std.Coin{ Denom: common.Denomination, Amount: singleRunCost.Amount - subAccount.Coins.AmountOf(common.Denomination), @@ -143,7 +143,7 @@ func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) }) // Figure out how many accounts can actually be funded - distributor, err := d.cli.GetAccount(accounts[0].GetAddress().String()) + distributor, err := d.cli.GetAccount(distributorKey.PubKey().Address().String()) if err != nil { return nil, fmt.Errorf("unable to fetch distributor account, %w", err) } @@ -178,10 +178,13 @@ func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) return nil, errInsufficientFunds } - // Locally keep track of the nonce, so - // there is no need to re-fetch the account again - // before signing a future tx - nonce := distributor.Sequence + var ( + // Locally keep track of the nonce, so + // there is no need to re-fetch the account again + // before signing a future tx + nonce = distributor.Sequence + defaultFee = std.NewFee(100000, common.DefaultGasFee) + ) fmt.Printf("Funding %d accounts...\n", len(shortAccounts)) bar := progressbar.Default(int64(len(shortAccounts)), "funding short accounts") @@ -196,11 +199,17 @@ func (d *Distributor) fundAccounts(accounts []keys.Info, singleRunCost std.Coin) Amount: std.NewCoins(account.missingFunds), }, }, - Fee: std.NewFee(100000, common.DefaultGasFee), + Fee: defaultFee, + } + + cfg := signer.SignCfg{ + ChainID: chainID, + AccountNumber: distributor.AccountNumber, + Sequence: nonce, } // Sign the transaction - if err := d.signer.SignTx(tx, distributor, nonce, common.EncryptPassword); err != nil { + if err := signer.SignTx(tx, distributorKey, cfg); err != nil { return nil, fmt.Errorf("unable to sign transaction, %w", err) } diff --git a/internal/distributor/distributor_test.go b/internal/distributor/distributor_test.go index 7a98374..75a4600 100644 --- a/internal/distributor/distributor_test.go +++ b/internal/distributor/distributor_test.go @@ -1,64 +1,17 @@ package distributor import ( - "fmt" "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/crypto/bip39" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/supernova/internal/common" + testutils "github.com/gnolang/supernova/internal/testing" "github.com/stretchr/testify/assert" ) -// generateMnemonic generates a new BIP39 mnemonic -func generateMnemonic(t *testing.T) string { - t.Helper() - - // Generate the entropy seed - entropySeed, err := bip39.NewEntropy(256) - if err != nil { - t.Fatalf("unable to generate entropy seed, %v", err) - } - - // Generate the actual mnemonic - mnemonic, err := bip39.NewMnemonic(entropySeed[:]) - if err != nil { - t.Fatalf("unable to generate mnemonic, %v", err) - } - - return mnemonic -} - -// generateAccounts generates mock keybase accounts -func generateAccounts(t *testing.T, count int) []keys.Info { - t.Helper() - - kb := keys.NewInMemory() - accounts := make([]keys.Info, count) - mnemonic := generateMnemonic(t) - - for i := 0; i < count; i++ { - info, err := kb.CreateAccount( - fmt.Sprintf("%s%d", common.KeybasePrefix, i), - mnemonic, - "", - common.EncryptPassword, - uint32(0), - uint32(i), - ) - if err != nil { - t.Fatalf("unable to create account with keybase, %v", err) - } - - accounts[i] = info - } - - return accounts -} - func TestDistributor_Distribute(t *testing.T) { t.Parallel() @@ -67,9 +20,9 @@ func TestDistributor_Distribute(t *testing.T) { singleCost = calculateRuntimeCosts(int64(numTx)) ) - getAccount := func(address string, accounts []keys.Info) keys.Info { + getAccount := func(address string, accounts []crypto.PrivKey) crypto.PrivKey { for _, account := range accounts { - if address == account.GetAddress().String() { + if address == account.PubKey().Address().String() { return account } } @@ -81,7 +34,7 @@ func TestDistributor_Distribute(t *testing.T) { t.Parallel() var ( - accounts = generateAccounts(t, 10) + accounts = testutils.GenerateAccounts(t, 10) mockClient = &mockClient{ getAccountFn: func(address string) (*gnoland.GnoAccount, error) { @@ -92,7 +45,7 @@ func TestDistributor_Distribute(t *testing.T) { return &gnoland.GnoAccount{ BaseAccount: *std.NewBaseAccount( - acc.GetAddress(), + acc.PubKey().Address(), std.NewCoins(singleCost), nil, 0, @@ -105,10 +58,15 @@ func TestDistributor_Distribute(t *testing.T) { d := NewDistributor( mockClient, - &mockSigner{}, ) - readyAccounts, err := d.Distribute(accounts, numTx) + // Extract the addresses + addresses := make([]crypto.Address, 0, len(accounts[1:])) + for _, account := range accounts[1:] { + addresses = append(addresses, account.PubKey().Address()) + } + + readyAccounts, err := d.Distribute(accounts[0], addresses, numTx, "dummy") if err != nil { t.Fatalf("unable to distribute funds, %v", err) } @@ -119,7 +77,7 @@ func TestDistributor_Distribute(t *testing.T) { // Make sure the accounts match for index, account := range accounts[1:] { - assert.Equal(t, account.GetAddress().String(), readyAccounts[index].GetAddress().String()) + assert.Equal(t, account.PubKey().Address().String(), readyAccounts[index].GetAddress().String()) } }) @@ -132,7 +90,7 @@ func TestDistributor_Distribute(t *testing.T) { } var ( - accounts = generateAccounts(t, 10) + accounts = testutils.GenerateAccounts(t, 10) mockClient = &mockClient{ getAccountFn: func(address string) (*gnoland.GnoAccount, error) { @@ -143,7 +101,7 @@ func TestDistributor_Distribute(t *testing.T) { return &gnoland.GnoAccount{ BaseAccount: *std.NewBaseAccount( - acc.GetAddress(), + acc.PubKey().Address(), std.NewCoins(emptyBalance), nil, 0, @@ -156,10 +114,15 @@ func TestDistributor_Distribute(t *testing.T) { d := NewDistributor( mockClient, - &mockSigner{}, ) - readyAccounts, err := d.Distribute(accounts, numTx) + // Extract the addresses + addresses := make([]crypto.Address, 0, len(accounts[1:])) + for _, account := range accounts[1:] { + addresses = append(addresses, account.PubKey().Address()) + } + + readyAccounts, err := d.Distribute(accounts[0], addresses, numTx, "dummy") assert.Nil(t, readyAccounts) assert.ErrorIs(t, err, errInsufficientFunds) @@ -174,9 +137,8 @@ func TestDistributor_Distribute(t *testing.T) { } var ( - accounts = generateAccounts(t, 10) + accounts = testutils.GenerateAccounts(t, 10) capturedBroadcasts = make([]*std.Tx, 0) - capturedNonce = uint64(0) mockClient = &mockClient{ getAccountFn: func(address string) (*gnoland.GnoAccount, error) { @@ -185,10 +147,10 @@ func TestDistributor_Distribute(t *testing.T) { t.Fatal("invalid account requested") } - if acc.GetName() == fmt.Sprintf("%s%d", common.KeybasePrefix, 0) { + if acc.Equals(accounts[0]) { return &gnoland.GnoAccount{ BaseAccount: *std.NewBaseAccount( - acc.GetAddress(), + acc.PubKey().Address(), std.NewCoins(std.Coin{ Denom: common.Denomination, Amount: int64(numTx) * common.DefaultGasFee.Add(singleCost).Amount, @@ -202,7 +164,7 @@ func TestDistributor_Distribute(t *testing.T) { return &gnoland.GnoAccount{ BaseAccount: *std.NewBaseAccount( - acc.GetAddress(), + acc.PubKey().Address(), std.NewCoins(emptyBalance), nil, 0, @@ -213,30 +175,6 @@ func TestDistributor_Distribute(t *testing.T) { broadcastTransactionFn: func(tx *std.Tx) error { capturedBroadcasts = append(capturedBroadcasts, tx) - return nil - }, - } - mockSigner = &mockSigner{ - signTxFn: func( - tx *std.Tx, - account *gnoland.GnoAccount, - nonce uint64, - passphrase string, - ) error { - if acc := getAccount(account.GetAddress().String(), accounts); acc == nil { - t.Fatal("invalid account") - } - - if passphrase != common.EncryptPassword { - t.Fatal("invalid passphrase") - } - - if nonce != capturedNonce { - t.Fatal("nonce not incrementing") - } - - capturedNonce++ - return nil }, } @@ -244,10 +182,15 @@ func TestDistributor_Distribute(t *testing.T) { d := NewDistributor( mockClient, - mockSigner, ) - readyAccounts, err := d.Distribute(accounts, numTx) + // Extract the addresses + addresses := make([]crypto.Address, 0, len(accounts[1:])) + for _, account := range accounts[1:] { + addresses = append(addresses, account.PubKey().Address()) + } + + readyAccounts, err := d.Distribute(accounts[0], addresses, numTx, "dummy") if err != nil { t.Fatalf("unable to distribute funds, %v", err) } @@ -258,13 +201,11 @@ func TestDistributor_Distribute(t *testing.T) { // Make sure the accounts match for index, account := range accounts[1:] { - assert.Equal(t, account.GetAddress().String(), readyAccounts[index].GetAddress().String()) + assert.Equal(t, account.PubKey().Address(), readyAccounts[index].GetAddress()) } // Check the broadcast transactions - if len(capturedBroadcasts) != len(accounts)-1 { - t.Fatal("invalid number of transactions broadcast") - } + assert.Len(t, capturedBroadcasts, len(accounts)-1) sendType := bank.MsgSend{}.Type() diff --git a/internal/distributor/mock_test.go b/internal/distributor/mock_test.go index c9f0b3c..b291a4d 100644 --- a/internal/distributor/mock_test.go +++ b/internal/distributor/mock_test.go @@ -30,17 +30,3 @@ func (m *mockClient) GetAccount(address string) (*gnoland.GnoAccount, error) { return nil, nil } - -type signTxDelegate func(*std.Tx, *gnoland.GnoAccount, uint64, string) error - -type mockSigner struct { - signTxFn signTxDelegate -} - -func (m *mockSigner) SignTx(tx *std.Tx, account *gnoland.GnoAccount, nonce uint64, passphrase string) error { - if m.signTxFn != nil { - return m.signTxFn(tx, account, nonce, passphrase) - } - - return nil -} diff --git a/internal/output.go b/internal/output.go index ff29d38..f9bbcf9 100644 --- a/internal/output.go +++ b/internal/output.go @@ -14,7 +14,7 @@ func displayResults(result *collector.RunResult) { w := tabwriter.NewWriter(os.Stdout, 10, 20, 2, ' ', 0) // TPS // - _, _ = fmt.Fprintln(w, fmt.Sprintf("\nTPS: %d", result.AverageTPS)) + _, _ = fmt.Fprintln(w, fmt.Sprintf("\nTPS: %.2f", result.AverageTPS)) // Block info // _, _ = fmt.Fprintln(w, "\nBlock #\tGas Used\tGas Limit\tTransactions\tUtilization") diff --git a/internal/pipeline.go b/internal/pipeline.go index 0ea4481..b648471 100644 --- a/internal/pipeline.go +++ b/internal/pipeline.go @@ -4,11 +4,11 @@ import ( "fmt" "time" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" "github.com/gnolang/supernova/internal/batcher" "github.com/gnolang/supernova/internal/client" "github.com/gnolang/supernova/internal/collector" - "github.com/gnolang/supernova/internal/common" "github.com/gnolang/supernova/internal/distributor" "github.com/gnolang/supernova/internal/runtime" "github.com/gnolang/supernova/internal/signer" @@ -21,34 +21,34 @@ type pipelineClient interface { collector.Client } -type pipelineSigner interface { - distributor.Signer -} - // Pipeline is the central run point // for the stress test type Pipeline struct { - cfg *Config // the run configuration - - keybase keys.Keybase // relevant keybase - cli pipelineClient // HTTP client connection - signer pipelineSigner // the transaction signer + cfg *Config // the run configuration + cli pipelineClient // HTTP client connection } // NewPipeline creates a new pipeline instance func NewPipeline(cfg *Config) (*Pipeline, error) { - kb := keys.NewInMemory() + var ( + cli *client.Client + err error + ) + + // Check which kind of client to create + if httpRegex.MatchString(cfg.URL) { + cli, err = client.NewHTTPClient(cfg.URL) + } else { + cli, err = client.NewWSClient(cfg.URL) + } - cli, err := client.NewHTTPClient(cfg.URL) if err != nil { - return nil, fmt.Errorf("unable to create HTTP client, %w", err) + return nil, fmt.Errorf("unable to create RPC client, %w", err) } return &Pipeline{ - cfg: cfg, - keybase: kb, - cli: cli, - signer: signer.NewKeybaseSigner(kb, cfg.ChainID), + cfg: cfg, + cli: cli, }, nil } @@ -59,31 +59,58 @@ func (p *Pipeline) Execute() error { txBatcher = batcher.NewBatcher(p.cli) txCollector = collector.NewCollector(p.cli) - txRuntime = runtime.GetRuntime(mode, p.signer) + txRuntime = runtime.GetRuntime(mode) ) // Initialize the accounts for the runtime - accounts, err := p.initializeAccounts() - if err != nil { - return err - } + accounts := p.initializeAccounts() // Predeploy any pending transactions - if err := prepareRuntime(mode, accounts, p.cli, txRuntime); err != nil { + if err := prepareRuntime( + mode, + accounts[0], + p.cfg.ChainID, + p.cli, + txRuntime, + ); err != nil { return err } + // Extract the addresses + addresses := make([]crypto.Address, 0, len(accounts[1:])) + for _, account := range accounts[1:] { + addresses = append(addresses, account.PubKey().Address()) + } + // Distribute the funds to sub-accounts - runAccounts, err := distributor.NewDistributor(p.cli, p.signer).Distribute( - accounts, + runAccounts, err := distributor.NewDistributor(p.cli).Distribute( + accounts[0], + addresses, p.cfg.Transactions, + p.cfg.ChainID, ) if err != nil { return fmt.Errorf("unable to distribute funds, %w", err) } + // Find which keys belong to the run accounts (not all initial accounts are run accounts) + runKeys := make([]crypto.PrivKey, 0, len(runAccounts)) + + for _, runAccount := range runAccounts { + for _, account := range accounts[1:] { + if account.PubKey().Address() == runAccount.GetAddress() { + runKeys = append(runKeys, account) + } + } + } + // Construct the transactions using the runtime - txs, err := txRuntime.ConstructTransactions(runAccounts, p.cfg.Transactions) + txs, err := txRuntime.ConstructTransactions( + runKeys, + runAccounts, + p.cfg.Transactions, + p.cfg.ChainID, + ) if err != nil { return fmt.Errorf("unable to construct transactions, %w", err) } @@ -111,37 +138,26 @@ func (p *Pipeline) Execute() error { } // initializeAccounts initializes the accounts needed for the stress test run -func (p *Pipeline) initializeAccounts() ([]keys.Info, error) { +func (p *Pipeline) initializeAccounts() []crypto.PrivKey { fmt.Printf("\n🧮 Initializing Accounts 🧮\n\n") - fmt.Printf("Generating sub-accounts...\n") var ( - accounts = make([]keys.Info, p.cfg.SubAccounts+1) + accounts = make([]crypto.PrivKey, p.cfg.SubAccounts+1) bar = progressbar.Default(int64(p.cfg.SubAccounts+1), "accounts initialized") + + seed = bip39.NewSeed(p.cfg.Mnemonic, "") ) // Register the accounts with the keybase for i := 0; i < int(p.cfg.SubAccounts)+1; i++ { - info, err := p.keybase.CreateAccount( - fmt.Sprintf("%s%d", common.KeybasePrefix, i), - p.cfg.Mnemonic, - "", - common.EncryptPassword, - uint32(0), - uint32(i), - ) - if err != nil { - return nil, fmt.Errorf("unable to create account with keybase, %w", err) - } - - accounts[i] = info + accounts[i] = signer.GenerateKeyFromSeed(seed, uint32(i)) _ = bar.Add(1) } fmt.Printf("✅ Successfully generated %d accounts\n", len(accounts)) - return accounts, nil + return accounts } // handleResults displays the results in the terminal, @@ -171,7 +187,8 @@ func (p *Pipeline) handleResults(runResult *collector.RunResult) error { // any pending transactions func prepareRuntime( mode runtime.Type, - accounts []keys.Info, + deployerKey crypto.PrivKey, + chainID string, cli pipelineClient, txRuntime runtime.Runtime, ) error { @@ -182,13 +199,13 @@ func prepareRuntime( fmt.Printf("\n✨ Starting Predeployment Procedure ✨\n\n") // Get the deployer account - deployer, err := cli.GetAccount(accounts[0].GetAddress().String()) + deployer, err := cli.GetAccount(deployerKey.PubKey().Address().String()) if err != nil { return fmt.Errorf("unable to fetch deployer account, %w", err) } // Get the predeploy transactions - predeployTxs, err := txRuntime.Initialize(deployer) + predeployTxs, err := txRuntime.Initialize(deployer, deployerKey, chainID) if err != nil { return fmt.Errorf("unable to initialize runtime, %w", err) } diff --git a/internal/runtime/common_deployment.go b/internal/runtime/common_deployment.go index f4abe28..6081261 100644 --- a/internal/runtime/common_deployment.go +++ b/internal/runtime/common_deployment.go @@ -5,35 +5,34 @@ import ( "path/filepath" "time" - "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) type commonDeployment struct { - signer Signer - deployDir string deployPathPrefix string } -func newCommonDeployment(signer Signer, deployDir, deployPrefix string) *commonDeployment { +func newCommonDeployment(deployDir, deployPrefix string) *commonDeployment { return &commonDeployment{ - signer: signer, deployDir: deployDir, deployPathPrefix: deployPrefix, } } -func (c *commonDeployment) Initialize(_ *gnoland.GnoAccount) ([]*std.Tx, error) { +func (c *commonDeployment) Initialize(_ std.Account, _ crypto.PrivKey, _ string) ([]*std.Tx, error) { // No extra setup needed for this runtime type return nil, nil } func (c *commonDeployment) ConstructTransactions( - accounts []*gnoland.GnoAccount, + keys []crypto.PrivKey, + accounts []std.Account, transactions uint64, + chainID string, ) ([]*std.Tx, error) { // Get absolute path to folder deployPathAbs, err := filepath.Abs(c.deployDir) @@ -43,7 +42,8 @@ func (c *commonDeployment) ConstructTransactions( var ( timestamp = time.Now().Unix() - getMsgFn = func(creator *gnoland.GnoAccount, index int) std.Msg { + + getMsgFn = func(creator std.Account, index int) std.Msg { memPkg := gnolang.ReadMemPackage( deployPathAbs, fmt.Sprintf("%s/stress_%d_%d", c.deployPathPrefix, timestamp, index), @@ -57,9 +57,10 @@ func (c *commonDeployment) ConstructTransactions( ) return constructTransactions( - c.signer, + keys, accounts, transactions, + chainID, getMsgFn, ) } diff --git a/internal/runtime/helper.go b/internal/runtime/helper.go index 6083aa9..1651030 100644 --- a/internal/runtime/helper.go +++ b/internal/runtime/helper.go @@ -3,21 +3,22 @@ package runtime import ( "fmt" - "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/supernova/internal/common" + "github.com/gnolang/supernova/internal/signer" "github.com/schollz/progressbar/v3" ) // msgFn defines the transaction message constructor -type msgFn func(creator *gnoland.GnoAccount, index int) std.Msg +type msgFn func(creator std.Account, index int) std.Msg // constructTransactions constructs and signs the transactions // using the passed in message generator and signer func constructTransactions( - signer Signer, - accounts []*gnoland.GnoAccount, + keys []crypto.PrivKey, + accounts []std.Account, transactions uint64, + chainID string, getMsg msgFn, ) ([]*std.Tx, error) { var ( @@ -35,7 +36,11 @@ func constructTransactions( for i := 0; i < int(transactions); i++ { // Generate the transaction - creator := accounts[i%len(accounts)] + var ( + creator = accounts[i%len(accounts)] + creatorKey = keys[i%len(accounts)] + accountNumber = creator.GetAccountNumber() + ) tx := &std.Tx{ Msgs: []std.Msg{getMsg(creator, i)}, @@ -43,19 +48,25 @@ func constructTransactions( } // Fetch the next account nonce - nonce, found := nonceMap[creator.AccountNumber] + nonce, found := nonceMap[creator.GetAccountNumber()] if !found { - nonce = creator.Sequence - nonceMap[creator.AccountNumber] = nonce + nonce = creator.GetSequence() + nonceMap[creator.GetAccountNumber()] = nonce } // Sign the transaction - if err := signer.SignTx(tx, creator, nonce, common.EncryptPassword); err != nil { + cfg := signer.SignCfg{ + ChainID: chainID, + AccountNumber: accountNumber, + Sequence: nonce, + } + + if err := signer.SignTx(tx, creatorKey, cfg); err != nil { return nil, fmt.Errorf("unable to sign transaction, %w", err) } // Increase the creator nonce locally - nonceMap[creator.AccountNumber] = nonce + 1 + nonceMap[accountNumber] = nonce + 1 // Mark the transaction as ready txs[i] = tx diff --git a/internal/runtime/helper_test.go b/internal/runtime/helper_test.go index 919ea4a..b0e2f04 100644 --- a/internal/runtime/helper_test.go +++ b/internal/runtime/helper_test.go @@ -6,12 +6,14 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/std" + testutils "github.com/gnolang/supernova/internal/testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // generateAccounts generates mock gno accounts -func generateAccounts(count int) []*gnoland.GnoAccount { - accounts := make([]*gnoland.GnoAccount, count) +func generateAccounts(count int) []std.Account { + accounts := make([]std.Account, count) for i := 0; i < count; i++ { accounts[i] = &gnoland.GnoAccount{ @@ -27,53 +29,33 @@ func generateAccounts(count int) []*gnoland.GnoAccount { func TestHelper_ConstructTransactions(t *testing.T) { t.Parallel() - accounts := generateAccounts(10) - nonceMap := make(map[uint64]uint64, len(accounts)) + var ( + accounts = generateAccounts(10) + accountKeys = testutils.GenerateAccounts(t, 10) + nonceMap = make(map[uint64]uint64, len(accounts)) + ) // Initialize the nonce map for _, account := range accounts { - nonceMap[account.AccountNumber] = 0 + nonceMap[account.GetAccountNumber()] = 0 } var ( - transactions = uint64(100) - capturedSigns = make([]*std.Tx, 0, transactions) - msg = vm.MsgAddPackage{} - - mockSigner = &mockSigner{ - signTxFn: func(tx *std.Tx, account *gnoland.GnoAccount, nonce uint64, _ string) error { - // Make sure the nonce is being incremented - if nonce != nonceMap[account.AccountNumber] { - t.Fatalf("invalid nonce for account") - } + transactions = uint64(100) + msg = vm.MsgAddPackage{} - capturedSigns = append(capturedSigns, tx) - nonceMap[account.AccountNumber]++ - - return nil - }, - } - getMsgFn = func(creator *gnoland.GnoAccount, index int) std.Msg { + getMsgFn = func(creator std.Account, index int) std.Msg { return msg } ) - txs, err := constructTransactions(mockSigner, accounts, transactions, getMsgFn) - if err != nil { - t.Fatalf("unable to construct transactions, %v", err) - } + txs, err := constructTransactions(accountKeys, accounts, transactions, "dummy", getMsgFn) + require.NoError(t, err) - if len(txs) != int(transactions) { - t.Fatalf("invalid number of transactions, %d", len(txs)) - } - - // Make sure each transaction was signed - if len(capturedSigns) != int(transactions) { - t.Fatalf("invalid number of transactions signed, %d", len(txs)) - } + assert.Len(t, txs, int(transactions)) // Make sure the constructed transactions are valid - for index, tx := range txs { + for _, tx := range txs { // Make sure the fee is valid assert.Equal(t, defaultDeployTxFee, tx.Fee) @@ -83,7 +65,6 @@ func TestHelper_ConstructTransactions(t *testing.T) { } assert.Equal(t, msg, tx.Msgs[0]) - - assert.Equal(t, capturedSigns[index], tx) + assert.NotEmpty(t, tx.Msgs[0].GetSigners()) } } diff --git a/internal/runtime/mock_test.go b/internal/runtime/mock_test.go deleted file mode 100644 index fc527c3..0000000 --- a/internal/runtime/mock_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package runtime - -import ( - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type signTxDelegate func(*std.Tx, *gnoland.GnoAccount, uint64, string) error - -type mockSigner struct { - signTxFn signTxDelegate -} - -func (m *mockSigner) SignTx(tx *std.Tx, account *gnoland.GnoAccount, nonce uint64, passphrase string) error { - if m.signTxFn != nil { - return m.signTxFn(tx, account, nonce, passphrase) - } - - return nil -} diff --git a/internal/runtime/realm_call.go b/internal/runtime/realm_call.go index 7517b50..2967913 100644 --- a/internal/runtime/realm_call.go +++ b/internal/runtime/realm_call.go @@ -5,11 +5,11 @@ import ( "path/filepath" "time" - "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/supernova/internal/common" + "github.com/gnolang/supernova/internal/signer" ) const ( @@ -17,18 +17,14 @@ const ( ) type realmCall struct { - signer Signer - realmPath string } -func newRealmCall(signer Signer) *realmCall { - return &realmCall{ - signer: signer, - } +func newRealmCall() *realmCall { + return &realmCall{} } -func (r *realmCall) Initialize(account *gnoland.GnoAccount) ([]*std.Tx, error) { +func (r *realmCall) Initialize(account std.Account, key crypto.PrivKey, chainID string) ([]*std.Tx, error) { // Get absolute path to folder deployPathAbs, err := filepath.Abs(realmLocation) if err != nil { @@ -54,7 +50,13 @@ func (r *realmCall) Initialize(account *gnoland.GnoAccount) ([]*std.Tx, error) { } // Sign it - if err := r.signer.SignTx(tx, account, account.Sequence, common.EncryptPassword); err != nil { + cfg := signer.SignCfg{ + ChainID: chainID, + AccountNumber: account.GetAccountNumber(), + Sequence: account.GetSequence(), + } + + if err := signer.SignTx(tx, key, cfg); err != nil { return nil, fmt.Errorf("unable to sign initialize transaction, %w", err) } @@ -62,12 +64,14 @@ func (r *realmCall) Initialize(account *gnoland.GnoAccount) ([]*std.Tx, error) { } func (r *realmCall) ConstructTransactions( - accounts []*gnoland.GnoAccount, + keys []crypto.PrivKey, + accounts []std.Account, transactions uint64, + chainID string, ) ([]*std.Tx, error) { - getMsgFn := func(creator *gnoland.GnoAccount, index int) std.Msg { + getMsgFn := func(creator std.Account, index int) std.Msg { return vm.MsgCall{ - Caller: creator.Address, + Caller: creator.GetAddress(), PkgPath: r.realmPath, Func: methodName, Args: []string{fmt.Sprintf("Account-%d", index)}, @@ -75,9 +79,10 @@ func (r *realmCall) ConstructTransactions( } return constructTransactions( - r.signer, + keys, accounts, transactions, + chainID, getMsgFn, ) } diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index beeedf1..3c6185c 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -1,7 +1,7 @@ package runtime import ( - "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/supernova/internal/common" ) @@ -25,27 +25,32 @@ var ( // and to predeploy (initialize) any infrastructure (package) type Runtime interface { // Initialize prepares any infrastructure transactions that are required - // to be executed before the stress test runs, if any - Initialize(*gnoland.GnoAccount) ([]*std.Tx, error) + // to be executed before the stress test runs, if any. + Initialize( + account std.Account, + key crypto.PrivKey, + chainID string, + ) ([]*std.Tx, error) // ConstructTransactions generates and signs the required transactions // that will be used in the stress test - ConstructTransactions(accounts []*gnoland.GnoAccount, transactions uint64) ([]*std.Tx, error) -} - -type Signer interface { - SignTx(tx *std.Tx, account *gnoland.GnoAccount, nonce uint64, passphrase string) error + ConstructTransactions( + keys []crypto.PrivKey, + accounts []std.Account, + transactions uint64, + chainID string, + ) ([]*std.Tx, error) } // GetRuntime fetches the specified runtime, if any -func GetRuntime(runtimeType Type, signer Signer) Runtime { +func GetRuntime(runtimeType Type) Runtime { switch runtimeType { case RealmCall: - return newRealmCall(signer) + return newRealmCall() case RealmDeployment: - return newCommonDeployment(signer, realmLocation, realmPathPrefix) + return newCommonDeployment(realmLocation, realmPathPrefix) case PackageDeployment: - return newCommonDeployment(signer, packageLocation, packagePathPrefix) + return newCommonDeployment(packageLocation, packagePathPrefix) default: return nil } diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index 0f6984b..a831ddc 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -8,6 +8,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/std" + testutils "github.com/gnolang/supernova/internal/testing" "github.com/stretchr/testify/assert" ) @@ -86,19 +87,20 @@ func TestRuntime_CommonDeployment(t *testing.T) { var ( transactions = uint64(100) accounts = generateAccounts(10) + accountKeys = testutils.GenerateAccounts(t, 10) ) // Get the runtime - r := GetRuntime(testCase.mode, &mockSigner{}) + r := GetRuntime(testCase.mode) // Make sure there is no initialization logic - initialTxs, err := r.Initialize(nil) + initialTxs, err := r.Initialize(accounts[0], accountKeys[0], "dummy") assert.Nil(t, initialTxs) assert.Nil(t, err) // Construct the transactions - txs, err := r.ConstructTransactions(accounts, transactions) + txs, err := r.ConstructTransactions(accountKeys, accounts, transactions, "dummy") if err != nil { t.Fatalf("unable to construct transactions, %v", err) } @@ -124,13 +126,14 @@ func TestRuntime_RealmCall(t *testing.T) { var ( transactions = uint64(100) accounts = generateAccounts(11) + accountKeys = testutils.GenerateAccounts(t, 11) ) // Get the runtime - r := GetRuntime(RealmCall, &mockSigner{}) + r := GetRuntime(RealmCall) // Make sure the initialization logic is present - initialTxs, err := r.Initialize(accounts[0]) + initialTxs, err := r.Initialize(accounts[0], accountKeys[0], "dummy") if err != nil { t.Fatalf("unable to generate init transactions, %v", err) } @@ -144,7 +147,7 @@ func TestRuntime_RealmCall(t *testing.T) { } // Construct the transactions - txs, err := r.ConstructTransactions(accounts, transactions) + txs, err := r.ConstructTransactions(accountKeys[1:], accounts[1:], transactions, "dummy") if err != nil { t.Fatalf("unable to construct transactions, %v", err) } diff --git a/internal/signer/keybase.go b/internal/signer/keybase.go deleted file mode 100644 index 8cba844..0000000 --- a/internal/signer/keybase.go +++ /dev/null @@ -1,77 +0,0 @@ -package signer - -import ( - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type KeybaseSigner struct { - chainID string - keybase keys.Keybase -} - -// NewKeybaseSigner creates a new signer instance -func NewKeybaseSigner(keybase keys.Keybase, chainID string) *KeybaseSigner { - return &KeybaseSigner{ - keybase: keybase, - chainID: chainID, - } -} - -// SignTx signs the given transaction by appending the -// signature to it -func (s *KeybaseSigner) SignTx( - tx *std.Tx, - account *gnoland.GnoAccount, - nonce uint64, - passphrase string, -) error { - // Fetch existing signers - signers := tx.GetSigners() - if tx.Signatures == nil { - for range signers { - tx.Signatures = append(tx.Signatures, std.Signature{ - PubKey: nil, // zero signature - Signature: nil, // zero signature - }) - } - } - - // Generate the signature - sigBytes, err := tx.GetSignBytes(s.chainID, account.AccountNumber, nonce) - if err != nil { - return fmt.Errorf("unable to get tx sign bytes, %w", err) - } - - signature, pub, err := s.keybase.Sign( - account.GetAddress().String(), - passphrase, - sigBytes, - ) - if err != nil { - return fmt.Errorf("unable to sign transaction, %w", err) - } - - addr := pub.Address() - found := false - - // Append the signature to the correct slot - for i := range tx.Signatures { - if signers[i] == addr { - found = true - tx.Signatures[i] = std.Signature{ - PubKey: pub, - Signature: signature, - } - } - } - - if !found { - return fmt.Errorf("unable to sign transaction with address %s", account.GetAddress()) - } - - return nil -} diff --git a/internal/signer/signer.go b/internal/signer/signer.go new file mode 100644 index 0000000..99b9514 --- /dev/null +++ b/internal/signer/signer.go @@ -0,0 +1,59 @@ +package signer + +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/hd" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// SignCfg specifies the sign configuration +type SignCfg struct { + ChainID string // the ID of the chain + AccountNumber uint64 // the account number of the signer + Sequence uint64 // the Sequence of the signer +} + +// SignTx signs the specified transaction using +// the provided key and config +func SignTx(tx *std.Tx, key crypto.PrivKey, cfg SignCfg) error { + // Get the sign bytes + signBytes, err := tx.GetSignBytes( + cfg.ChainID, + cfg.AccountNumber, + cfg.Sequence, + ) + if err != nil { + return fmt.Errorf("unable to get tx signature payload, %w", err) + } + + // Sign the transaction + signature, err := key.Sign(signBytes) + if err != nil { + return fmt.Errorf("unable to sign transaction, %w", err) + } + + // Save the signature + tx.Signatures = append(tx.Signatures, std.Signature{ + PubKey: key.PubKey(), + Signature: signature, + }) + + return nil +} + +// GenerateKeyFromSeed generates a private key from +// the provided seed and index +func GenerateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { + pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) + + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + + //nolint:errcheck // This derivation can never error out, since the path params + // are always going to be valid + derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) + + return secp256k1.PrivKeySecp256k1(derivedPriv) +} diff --git a/internal/testing/testing.go b/internal/testing/testing.go new file mode 100644 index 0000000..c1401e2 --- /dev/null +++ b/internal/testing/testing.go @@ -0,0 +1,45 @@ +package testutils + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/supernova/internal/signer" +) + +// GenerateMnemonic generates a new BIP39 mnemonic +func GenerateMnemonic(t *testing.T) string { + t.Helper() + + // Generate the entropy seed + entropySeed, err := bip39.NewEntropy(256) + if err != nil { + t.Fatalf("unable to generate entropy seed, %v", err) + } + + // Generate the actual mnemonic + mnemonic, err := bip39.NewMnemonic(entropySeed[:]) + if err != nil { + t.Fatalf("unable to generate mnemonic, %v", err) + } + + return mnemonic +} + +// GenerateAccounts generates mock keybase accounts +func GenerateAccounts(t *testing.T, count int) []crypto.PrivKey { + t.Helper() + + var ( + accounts = make([]crypto.PrivKey, count) + mnemonic = GenerateMnemonic(t) + seed = bip39.NewSeed(mnemonic, "") + ) + + for i := 0; i < count; i++ { + accounts[i] = signer.GenerateKeyFromSeed(seed, uint32(i)) + } + + return accounts +} diff --git a/scripts/p/package.gno b/scripts/p/package.gno index ca3d716..fe08bdf 100644 --- a/scripts/p/package.gno +++ b/scripts/p/package.gno @@ -5,6 +5,7 @@ type Language string const ( French Language = "french" Italian Language = "italian" + Spanish Language = "spanish" Hindi Language = "hindi" Bulgarian Language = "bulgarian" Serbian Language = "serbian" @@ -18,6 +19,8 @@ func GetGreeting(language Language) string { return "Bonjour" case Italian: return "Ciao" + case Spanish: + return "Hola" case Hindi: return "नमस्ते" case Bulgarian: