From eecd0ffc505c3b8104e9c62030afe27b175fc9c0 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:49:09 +0100 Subject: [PATCH] feat: add InMemory `gnoland` node (#1241) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- .github/workflows/gnoland.yml | 8 +- gno.land/Makefile | 4 +- gno.land/cmd/genesis/balances_add.go | 131 ++----- gno.land/cmd/genesis/balances_add_test.go | 107 +++--- gno.land/cmd/genesis/balances_export_test.go | 31 +- gno.land/cmd/genesis/balances_remove.go | 2 +- gno.land/cmd/genesis/balances_remove_test.go | 15 +- gno.land/cmd/genesis/types.go | 22 +- gno.land/cmd/genesis/verify.go | 5 +- gno.land/cmd/genesis/verify_test.go | 8 +- gno.land/cmd/gnoland/root.go | 3 +- gno.land/cmd/gnoland/start.go | 294 +++++---------- gno.land/cmd/gnoland/testdata/addpkg.txtar | 11 +- gno.land/cmd/gnoweb/main.go | 8 +- gno.land/cmd/gnoweb/main_test.go | 34 +- gno.land/pkg/gnoland/app.go | 47 ++- gno.land/pkg/gnoland/genesis.go | 126 +++++++ gno.land/pkg/gnoland/node_inmemory.go | 147 ++++++++ gno.land/pkg/gnoland/types.go | 65 +++- gno.land/pkg/gnoland/types_test.go | 98 +++++ gno.land/pkg/integration/gnoland.go | 334 ------------------ gno.land/pkg/integration/testing.go | 39 ++ .../pkg/integration/testing_integration.go | 174 ++++----- gno.land/pkg/integration/testing_node.go | 184 ++++++++++ 24 files changed, 1010 insertions(+), 887 deletions(-) create mode 100644 gno.land/pkg/gnoland/genesis.go create mode 100644 gno.land/pkg/gnoland/node_inmemory.go create mode 100644 gno.land/pkg/gnoland/types_test.go delete mode 100644 gno.land/pkg/integration/gnoland.go create mode 100644 gno.land/pkg/integration/testing.go create mode 100644 gno.land/pkg/integration/testing_node.go diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 95cb5fa8ce0..d305bed2dcd 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -60,9 +60,7 @@ jobs: - _test.gnoland - _test.gnokey - _test.pkgs - # XXX: test broken, should be rewritten to run an inmemory localnode - # Re-add to makefile when fixed. Tracked here: https://github.com/gnolang/gno/issues/1222 - #- _test.gnoweb + - _test.gnoweb runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -78,7 +76,7 @@ jobs: export LOG_DIR="${{ runner.temp }}/logs/test-${{ matrix.goversion }}-gnoland" make ${{ matrix.args }} - name: Upload Test Log - if: always() + if: always() uses: actions/upload-artifact@v3 with: name: logs-test-gnoland-go${{ matrix.goversion }} @@ -101,7 +99,7 @@ jobs: uses: codecov/codecov-action@v3 with: directory: ${{ runner.temp }}/coverage - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} docker-integration: diff --git a/gno.land/Makefile b/gno.land/Makefile index 22b9ec24650..29c192e9987 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -48,9 +48,7 @@ fmt: ######################################## # Test suite .PHONY: test -test: _test.gnoland _test.gnokey _test.pkgs -# XXX: _test.gnoweb is currently disabled. If fixed, re-enable here and in CI. -# https://github.com/gnolang/gno/issues/1222 +test: _test.gnoland _test.gnoweb _test.gnokey _test.pkgs GOTEST_FLAGS ?= -v -p 1 -timeout=30m diff --git a/gno.land/cmd/genesis/balances_add.go b/gno.land/cmd/genesis/balances_add.go index 276e48690a8..8df193c770c 100644 --- a/gno.land/cmd/genesis/balances_add.go +++ b/gno.land/cmd/genesis/balances_add.go @@ -8,32 +8,22 @@ import ( "fmt" "io" "os" - "regexp" - "strconv" "strings" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "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/gno/gno.land/pkg/sdk/vm" ) -var ( - balanceRegex = regexp.MustCompile(`^(\w+)=(\d+)ugnot$`) - amountRegex = regexp.MustCompile(`^(\d+)ugnot$`) -) - var ( errNoBalanceSource = errors.New("at least one balance source must be set") errBalanceParsingAborted = errors.New("balance parsing aborted") - errInvalidBalanceFormat = errors.New("invalid balance format encountered") errInvalidAddress = errors.New("invalid address encountered") - errInvalidAmount = errors.New("invalid amount encountered") ) type balancesAddCfg struct { @@ -152,7 +142,7 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io *commands.IO) // Construct the initial genesis balance sheet state := genesis.AppState.(gnoland.GnoGenesisState) - genesisBalances, err := extractGenesisBalances(state) + genesisBalances, err := mapGenesisBalancesFromState(state) if err != nil { return err } @@ -190,12 +180,11 @@ func getBalancesFromEntries(entries []string) (accountBalances, error) { balances := make(accountBalances) for _, entry := range entries { - accountBalance, err := getBalanceFromEntry(entry) - if err != nil { - return nil, fmt.Errorf("unable to extract balance data, %w", err) + var balance gnoland.Balance + if err := balance.Parse(entry); err != nil { + return nil, fmt.Errorf("unable to parse balance entry: %w", err) } - - balances[accountBalance.address] = accountBalance.amount + balances[balance.Address] = balance } return balances, nil @@ -220,12 +209,12 @@ func getBalancesFromSheet(sheet io.Reader) (accountBalances, error) { continue } - accountBalance, err := getBalanceFromEntry(entry) - if err != nil { + var balance gnoland.Balance + if err := balance.Parse(entry); err != nil { return nil, fmt.Errorf("unable to extract balance data, %w", err) } - balances[accountBalance.address] = accountBalance.amount + balances[balance.Address] = balance } if err := scanner.Err(); err != nil { @@ -262,21 +251,19 @@ func getBalancesFromTransactions( if err := amino.UnmarshalJSON(line, &tx); err != nil { io.ErrPrintfln( - "invalid amino JSON encountered: %s", + "invalid amino JSON encountered: %q", string(line), ) continue } - feeAmount, err := getAmountFromEntry(tx.Fee.GasFee.String()) - if err != nil { + feeAmount := std.NewCoins(tx.Fee.GasFee) + if feeAmount.AmountOf("ugnot") <= 0 { io.ErrPrintfln( - "invalid gas fee amount encountered: %s", + "invalid gas fee amount encountered: %q", tx.Fee.GasFee.String(), ) - - continue } for _, msg := range tx.Msgs { @@ -286,13 +273,12 @@ func getBalancesFromTransactions( msgSend := msg.(bank.MsgSend) - sendAmount, err := getAmountFromEntry(msgSend.Amount.String()) - if err != nil { + sendAmount := msgSend.Amount + if sendAmount.AmountOf("ugnot") <= 0 { io.ErrPrintfln( "invalid send amount encountered: %s", msgSend.Amount.String(), ) - continue } @@ -304,27 +290,35 @@ func getBalancesFromTransactions( // causes an accounts balance to go < 0. In these cases, // we initialize the account (it is present in the balance sheet), but // with the balance of 0 - from := balances[msgSend.FromAddress] - to := balances[msgSend.ToAddress] - to += sendAmount + from := balances[msgSend.FromAddress].Amount + to := balances[msgSend.ToAddress].Amount + + to = to.Add(sendAmount) - if from < sendAmount || from < feeAmount { + if from.IsAllLT(sendAmount) || from.IsAllLT(feeAmount) { // Account cannot cover send amount / fee // (see message above) - from = 0 + from = std.NewCoins(std.NewCoin("ugnot", 0)) } - if from > sendAmount { - from -= sendAmount + if from.IsAllGT(sendAmount) { + from = from.Sub(sendAmount) } - if from > feeAmount { - from -= feeAmount + if from.IsAllGT(feeAmount) { + from = from.Sub(feeAmount) } - balances[msgSend.FromAddress] = from - balances[msgSend.ToAddress] = to + // Set new balance + balances[msgSend.FromAddress] = gnoland.Balance{ + Address: msgSend.FromAddress, + Amount: from, + } + balances[msgSend.ToAddress] = gnoland.Balance{ + Address: msgSend.ToAddress, + Amount: to, + } } } } @@ -340,65 +334,14 @@ func getBalancesFromTransactions( return balances, nil } -// getAmountFromEntry -func getAmountFromEntry(entry string) (int64, error) { - matches := amountRegex.FindStringSubmatch(entry) - - // Check if there is a match - if len(matches) != 2 { - return 0, fmt.Errorf( - "invalid amount, %s", - entry, - ) - } - - amount, err := strconv.ParseInt(matches[1], 10, 64) - if err != nil { - return 0, fmt.Errorf("invalid amount, %s", matches[1]) - } - - return amount, nil -} - -// getBalanceFromEntry extracts the account balance information -// from a single line in the form of:
=ugnot -func getBalanceFromEntry(entry string) (*accountBalance, error) { - matches := balanceRegex.FindStringSubmatch(entry) - if len(matches) != 3 { - return nil, fmt.Errorf("%w, %s", errInvalidBalanceFormat, entry) - } - - // Validate the address - address, err := crypto.AddressFromString(matches[1]) - if err != nil { - return nil, fmt.Errorf("%w, %w", errInvalidAddress, err) - } - - // Validate the amount - amount, err := strconv.ParseInt(matches[2], 10, 64) - if err != nil { - return nil, fmt.Errorf("%w, %w", errInvalidAmount, err) - } - - return &accountBalance{ - address: address, - amount: amount, - }, nil -} - -// extractGenesisBalances extracts the initial account balances from the +// mapGenesisBalancesFromState extracts the initial account balances from the // genesis app state -func extractGenesisBalances(state gnoland.GnoGenesisState) (accountBalances, error) { +func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (accountBalances, error) { // Construct the initial genesis balance sheet genesisBalances := make(accountBalances) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - if err != nil { - return nil, fmt.Errorf("invalid genesis balance entry, %w", err) - } - - genesisBalances[accountBalance.address] = accountBalance.amount + for _, balance := range state.Balances { + genesisBalances[balance.Address] = balance } return genesisBalances, nil diff --git a/gno.land/cmd/genesis/balances_add_test.go b/gno.land/cmd/genesis/balances_add_test.go index f986ee85274..73e2fe148a2 100644 --- a/gno.land/cmd/genesis/balances_add_test.go +++ b/gno.land/cmd/genesis/balances_add_test.go @@ -98,7 +98,7 @@ func TestGenesis_Balances_Add(t *testing.T) { tempGenesis.Name(), } - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) for _, dummyKey := range dummyKeys { args = append(args, "--single") @@ -107,7 +107,7 @@ func TestGenesis_Balances_Add(t *testing.T) { fmt.Sprintf( "%s=%dugnot", dummyKey.Address().String(), - amount, + amount.AmountOf("ugnot"), ), ) } @@ -127,16 +127,13 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, amount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) found = true break @@ -144,7 +141,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -159,7 +156,7 @@ func TestGenesis_Balances_Add(t *testing.T) { require.NoError(t, genesis.SaveAs(tempGenesis.Name())) dummyKeys := getDummyKeys(t, 10) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) balances := make([]string, len(dummyKeys)) @@ -170,7 +167,7 @@ func TestGenesis_Balances_Add(t *testing.T) { balances[index] = fmt.Sprintf( "%s=%dugnot", key.Address().String(), - amount, + amount.AmountOf("ugnot"), ) } @@ -207,16 +204,13 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, amount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) found = true break @@ -224,7 +218,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -240,8 +234,8 @@ func TestGenesis_Balances_Add(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + amount = std.NewCoins(std.NewCoin("ugnot", 10)) + amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) gasFee = std.NewCoin("ugnot", 1000000) txs = make([]std.Tx, 0) ) @@ -309,10 +303,7 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false @@ -321,11 +312,11 @@ func TestGenesis_Balances_Add(t *testing.T) { if index == 0 { // the first address should // have a balance of 0 - checkAmount = 0 + checkAmount = std.NewCoins(std.NewCoin("ugnot", 0)) } - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, checkAmount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.True(t, balance.Amount.IsEqual(checkAmount)) found = true break @@ -333,7 +324,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -349,12 +340,11 @@ func TestGenesis_Balances_Add(t *testing.T) { genesis := getDefaultGenesis() state := gnoland.GnoGenesisState{ // Set an initial balance value - Balances: []string{ - fmt.Sprintf( - "%s=%dugnot", - dummyKeys[0].Address().String(), - 100, - ), + Balances: []gnoland.Balance{ + { + Address: dummyKeys[0].Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + }, }, } genesis.AppState = state @@ -369,7 +359,7 @@ func TestGenesis_Balances_Add(t *testing.T) { tempGenesis.Name(), } - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) for _, dummyKey := range dummyKeys { args = append(args, "--single") @@ -378,7 +368,7 @@ func TestGenesis_Balances_Add(t *testing.T) { fmt.Sprintf( "%s=%dugnot", dummyKey.Address().String(), - amount, + amount.AmountOf("ugnot"), ), ) } @@ -398,16 +388,13 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, amount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) found = true break @@ -415,7 +402,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -429,7 +416,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { // Generate dummy keys dummyKeys := getDummyKeys(t, 2) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) balances := make([]string, len(dummyKeys)) @@ -437,7 +424,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balances[index] = fmt.Sprintf( "%s=%dugnot", key.Address().String(), - amount, + amount.AmountOf("ugnot"), ) } @@ -447,7 +434,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()]) + assert.Equal(t, amount, balanceMap[key.Address()].Amount) } }) @@ -461,7 +448,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := getBalancesFromEntries(balances) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidBalanceFormat.Error()) + assert.ErrorContains(t, err, "malformed entry") }) t.Run("malformed balance, invalid address", func(t *testing.T) { @@ -474,7 +461,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := getBalancesFromEntries(balances) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidAddress.Error()) + assert.ErrorContains(t, err, "invalid address") }) t.Run("malformed balance, invalid amount", func(t *testing.T) { @@ -493,7 +480,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := getBalancesFromEntries(balances) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidAmount.Error()) + assert.ErrorContains(t, err, "invalid amount") }) } @@ -505,7 +492,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // Generate dummy keys dummyKeys := getDummyKeys(t, 2) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) balances := make([]string, len(dummyKeys)) @@ -513,7 +500,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balances[index] = fmt.Sprintf( "%s=%dugnot", key.Address().String(), - amount, + amount.AmountOf("ugnot"), ) } @@ -524,7 +511,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()]) + assert.Equal(t, amount, balanceMap[key.Address()].Amount) } }) @@ -546,7 +533,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balanceMap, err := getBalancesFromSheet(reader) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidAmount.Error()) + assert.ErrorContains(t, err, "invalid amount") }) } @@ -558,8 +545,8 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + amount = std.NewCoins(std.NewCoin("ugnot", 10)) + amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) gasFee = std.NewCoin("ugnot", 1000000) txs = make([]std.Tx, 0) ) @@ -605,10 +592,10 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys[1:] { - assert.Equal(t, amount, balanceMap[key.Address()]) + assert.Equal(t, amount, balanceMap[key.Address()].Amount) } - assert.Equal(t, int64(0), balanceMap[sender.Address()]) + assert.Equal(t, std.Coins{}, balanceMap[sender.Address()].Amount) }) t.Run("malformed transaction, invalid fee amount", func(t *testing.T) { @@ -616,8 +603,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) gasFee = std.NewCoin("gnos", 1) // invalid fee txs = make([]std.Tx, 0) ) @@ -669,8 +655,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("gnogno", amount)) // invalid send amount + amountCoins = std.NewCoins(std.NewCoin("gnogno", 10)) // invalid send amount gasFee = std.NewCoin("ugnot", 1) txs = make([]std.Tx, 0) ) diff --git a/gno.land/cmd/genesis/balances_export_test.go b/gno.land/cmd/genesis/balances_export_test.go index 33e4f7bc800..d7441fd438f 100644 --- a/gno.land/cmd/genesis/balances_export_test.go +++ b/gno.land/cmd/genesis/balances_export_test.go @@ -3,31 +3,30 @@ package main import ( "bufio" "context" - "fmt" "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// getDummyBalanceLines generates dummy balance lines -func getDummyBalanceLines(t *testing.T, count int) []string { +// getDummyBalances generates dummy balance lines +func getDummyBalances(t *testing.T, count int) []gnoland.Balance { t.Helper() dummyKeys := getDummyKeys(t, count) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) - balances := make([]string, len(dummyKeys)) + balances := make([]gnoland.Balance, len(dummyKeys)) for index, key := range dummyKeys { - balances[index] = fmt.Sprintf( - "%s=%dugnot", - key.Address().String(), - amount, - ) + balances[index] = gnoland.Balance{ + Address: key.Address(), + Amount: amount, + } } return balances @@ -85,7 +84,7 @@ func TestGenesis_Balances_Export(t *testing.T) { genesis := getDefaultGenesis() genesis.AppState = gnoland.GnoGenesisState{ - Balances: getDummyBalanceLines(t, 1), + Balances: getDummyBalances(t, 1), } require.NoError(t, genesis.SaveAs(tempGenesis.Name())) @@ -107,7 +106,7 @@ func TestGenesis_Balances_Export(t *testing.T) { t.Parallel() // Generate dummy balances - balances := getDummyBalanceLines(t, 10) + balances := getDummyBalances(t, 10) tempGenesis, cleanup := testutils.NewTestFile(t) t.Cleanup(cleanup) @@ -139,9 +138,13 @@ func TestGenesis_Balances_Export(t *testing.T) { // Validate the transactions were written down scanner := bufio.NewScanner(outputFile) - outputBalances := make([]string, 0) + outputBalances := make([]gnoland.Balance, 0) for scanner.Scan() { - outputBalances = append(outputBalances, scanner.Text()) + var balance gnoland.Balance + err := balance.Parse(scanner.Text()) + require.NoError(t, err) + + outputBalances = append(outputBalances, balance) } require.NoError(t, scanner.Err()) diff --git a/gno.land/cmd/genesis/balances_remove.go b/gno.land/cmd/genesis/balances_remove.go index f7e9092dc3b..f4286d95ad2 100644 --- a/gno.land/cmd/genesis/balances_remove.go +++ b/gno.land/cmd/genesis/balances_remove.go @@ -71,7 +71,7 @@ func execBalancesRemove(cfg *balancesRemoveCfg, io *commands.IO) error { // Construct the initial genesis balance sheet state := genesis.AppState.(gnoland.GnoGenesisState) - genesisBalances, err := extractGenesisBalances(state) + genesisBalances, err := mapGenesisBalancesFromState(state) if err != nil { return err } diff --git a/gno.land/cmd/genesis/balances_remove_test.go b/gno.land/cmd/genesis/balances_remove_test.go index 29179c43604..b9d10d0db08 100644 --- a/gno.land/cmd/genesis/balances_remove_test.go +++ b/gno.land/cmd/genesis/balances_remove_test.go @@ -2,12 +2,12 @@ package main import ( "context" - "fmt" "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -70,12 +70,11 @@ func TestGenesis_Balances_Remove(t *testing.T) { genesis := getDefaultGenesis() state := gnoland.GnoGenesisState{ // Set an initial balance value - Balances: []string{ - fmt.Sprintf( - "%s=%dugnot", - dummyKey.Address().String(), - 100, - ), + Balances: []gnoland.Balance{ + { + Address: dummyKey.Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + }, }, } genesis.AppState = state @@ -118,7 +117,7 @@ func TestGenesis_Balances_Remove(t *testing.T) { genesis := getDefaultGenesis() state := gnoland.GnoGenesisState{ - Balances: []string{}, // Empty initial balance + Balances: []gnoland.Balance{}, // Empty initial balance } genesis.AppState = state require.NoError(t, genesis.SaveAs(tempGenesis.Name())) diff --git a/gno.land/cmd/genesis/types.go b/gno.land/cmd/genesis/types.go index 208eaddb6da..dba39ea8ec1 100644 --- a/gno.land/cmd/genesis/types.go +++ b/gno.land/cmd/genesis/types.go @@ -1,8 +1,7 @@ package main import ( - "fmt" - + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -39,23 +38,14 @@ func (i *txStore) leftMerge(b txStore) error { return nil } -type ( - accountBalances map[types.Address]int64 // address -> balance (ugnot) - accountBalance struct { - address types.Address - amount int64 - } -) +type accountBalances map[types.Address]gnoland.Balance // address -> balance (ugnot) // toList linearizes the account balances map -func (a accountBalances) toList() []string { - balances := make([]string, 0, len(a)) +func (a accountBalances) toList() []gnoland.Balance { + balances := make([]gnoland.Balance, 0, len(a)) - for address, balance := range a { - balances = append( - balances, - fmt.Sprintf("%s=%dugnot", address, balance), - ) + for _, balance := range a { + balances = append(balances, balance) } return balances diff --git a/gno.land/cmd/genesis/verify.go b/gno.land/cmd/genesis/verify.go index ba51f5801f6..6c877ca51ec 100644 --- a/gno.land/cmd/genesis/verify.go +++ b/gno.land/cmd/genesis/verify.go @@ -9,7 +9,6 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" ) var errInvalidGenesisState = errors.New("invalid genesis state type") @@ -68,8 +67,8 @@ func execVerify(cfg *verifyCfg, io *commands.IO) error { // Validate the initial balances for _, balance := range state.Balances { - if _, parseErr := std.ParseCoins(balance); parseErr != nil { - return fmt.Errorf("invalid balance %s, %w", balance, parseErr) + if err := balance.Verify(); err != nil { + return fmt.Errorf("invalid balance: %w", err) } } } diff --git a/gno.land/cmd/genesis/verify_test.go b/gno.land/cmd/genesis/verify_test.go index fcc5305b9d0..8388949898b 100644 --- a/gno.land/cmd/genesis/verify_test.go +++ b/gno.land/cmd/genesis/verify_test.go @@ -44,7 +44,7 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ - Balances: []string{}, + Balances: []gnoland.Balance{}, Txs: []std.Tx{ {}, }, @@ -74,8 +74,8 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ - Balances: []string{ - "dummybalance", + Balances: []gnoland.Balance{ + {}, }, Txs: []std.Tx{}, } @@ -103,7 +103,7 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ - Balances: []string{}, + Balances: []gnoland.Balance{}, Txs: []std.Tx{}, } diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index cf2a6252478..5b2cbe0e4fe 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -11,8 +11,7 @@ import ( ) func main() { - io := commands.NewDefaultIO() - cmd := newRootCmd(io) + cmd := newRootCmd(commands.NewDefaultIO()) if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index a42e1df1bf0..618f4f87a09 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -1,21 +1,15 @@ package main import ( - "bufio" "context" "errors" "flag" "fmt" - "os" "path/filepath" "strings" "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" - vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/gnomod" - "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -32,13 +26,14 @@ import ( ) type startCfg struct { + gnoRootDir string skipFailingGenesisTxs bool skipStart bool genesisBalancesFile string genesisTxsFile string chainID string genesisRemote string - rootDir string + dataDir string genesisMaxVMCycles int64 config string @@ -64,6 +59,10 @@ func newStartCmd(io *commands.IO) *commands.Command { } func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { + gnoroot := gnoland.MustGuessGnoRootDir() + defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") + defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.txt") + fs.BoolVar( &c.skipFailingGenesisTxs, "skip-failing-genesis-txs", @@ -81,14 +80,14 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.genesisBalancesFile, "genesis-balances-file", - "./genesis/genesis_balances.txt", + defaultGenesisBalancesFile, "initial distribution file", ) fs.StringVar( &c.genesisTxsFile, "genesis-txs-file", - "./genesis/genesis_txs.txt", + defaultGenesisTxsFile, "initial txs to replay", ) @@ -100,8 +99,16 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { ) fs.StringVar( - &c.rootDir, - "root-dir", + &c.gnoRootDir, + "gnoroot-dir", + gnoroot, + "the root directory of the gno repository", + ) + + // XXX: Use home directory for this + fs.StringVar( + &c.dataDir, + "data-dir", "testdir", "directory for config and data", ) @@ -156,11 +163,19 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "", fmt.Sprintf("path for the file tx event store (required if event store is '%s')", file.EventStoreType), ) + + // XXX(deprecated): use data-dir instead + fs.StringVar( + &c.dataDir, + "root-dir", + "testdir", + "deprecated: use data-dir instead - directory for config and data", + ) } func execStart(c *startCfg, io *commands.IO) error { logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) - rootDir := c.rootDir + dataDir := c.dataDir var ( cfg *config.Config @@ -174,39 +189,28 @@ func execStart(c *startCfg, io *commands.IO) error { cfg, loadCfgErr = config.LoadConfigFile(c.nodeConfigPath) } else { // Load the default node configuration - cfg, loadCfgErr = config.LoadOrMakeConfigWithOptions(rootDir, nil) + cfg, loadCfgErr = config.LoadOrMakeConfigWithOptions(dataDir, nil) } if loadCfgErr != nil { return fmt.Errorf("unable to load node configuration, %w", loadCfgErr) } - // create priv validator first. - // need it to generate genesis.json - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() - priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) - - // write genesis file if missing. - genesisFilePath := filepath.Join(rootDir, cfg.Genesis) - - genesisTxs, genesisTxsErr := loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote) - if genesisTxsErr != nil { - return fmt.Errorf("unable to load genesis txs, %w", genesisTxsErr) - } + // Write genesis file if missing. + genesisFilePath := filepath.Join(dataDir, cfg.Genesis) if !osm.FileExists(genesisFilePath) { - genDoc, err := makeGenesisDoc( - priv.GetPubKey(), - c.chainID, - c.genesisBalancesFile, - genesisTxs, - ) - if err != nil { - return fmt.Errorf("unable to generate genesis.json, %w", err) + // Create priv validator first. + // Need it to generate genesis.json + newPrivValKey := cfg.PrivValidatorKeyFile() + newPrivValState := cfg.PrivValidatorStateFile() + priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) + pk := priv.GetPubKey() + + // Generate genesis.json file + if err := generateGenesisFile(genesisFilePath, pk, c); err != nil { + return fmt.Errorf("unable to generate genesis file: %w", err) } - - writeGenesisFile(genDoc, genesisFilePath) } // Initialize the indexer config @@ -214,15 +218,13 @@ func execStart(c *startCfg, io *commands.IO) error { if err != nil { return fmt.Errorf("unable to parse indexer config, %w", err) } - cfg.TxEventStore = txEventStoreCfg - // create application and node. - gnoApp, err := gnoland.NewApp(rootDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) + // Create application and node. + gnoApp, err := gnoland.NewApp(dataDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) if err != nil { return fmt.Errorf("error in creating new app: %w", err) } - cfg.LocalApp = gnoApp gnoNode, err := node.DefaultNewNode(cfg, logger) @@ -233,8 +235,7 @@ func execStart(c *startCfg, io *commands.IO) error { fmt.Fprintln(io.Err, "Node created.") if c.skipStart { - fmt.Fprintln(io.Err, "'--skip-start' is set. Exiting.") - + io.ErrPrintln("'--skip-start' is set. Exiting.") return nil } @@ -242,215 +243,96 @@ func execStart(c *startCfg, io *commands.IO) error { return fmt.Errorf("error in start node: %w", err) } - // run forever osm.TrapSignal(func() { if gnoNode.IsRunning() { _ = gnoNode.Stop() } }) - select {} // run forever + // Run forever + select {} } -// getTxEventStoreConfig constructs an event store config from provided user options -func getTxEventStoreConfig(c *startCfg) (*eventstorecfg.Config, error) { - var cfg *eventstorecfg.Config - - switch c.txEventStoreType { - case file.EventStoreType: - if c.txEventStorePath == "" { - return nil, errors.New("unspecified file transaction indexer path") - } - - // Fill out the configuration - cfg = &eventstorecfg.Config{ - EventStoreType: file.EventStoreType, - Params: map[string]any{ - file.Path: c.txEventStorePath, - }, - } - default: - cfg = eventstorecfg.DefaultEventStoreConfig() - } - - return cfg, nil -} - -// Makes a local test genesis doc with local privValidator. -func makeGenesisDoc( - pvPub crypto.PubKey, - chainID string, - genesisBalancesFile string, - genesisTxs []std.Tx, -) (*bft.GenesisDoc, error) { +func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error { gen := &bft.GenesisDoc{} - gen.GenesisTime = time.Now() - gen.ChainID = chainID + gen.ChainID = c.chainID gen.ConsensusParams = abci.ConsensusParams{ Block: &abci.BlockParams{ // TODO: update limits. - MaxTxBytes: 1000000, // 1MB, - MaxDataBytes: 2000000, // 2MB, - MaxGas: 10000000, // 10M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 10_0000_00, // 10M gas + TimeIotaMS: 100, // 100ms }, } + gen.Validators = []bft.GenesisValidator{ { - Address: pvPub.Address(), - PubKey: pvPub, + Address: pk.Address(), + PubKey: pk, Power: 10, Name: "testvalidator", }, } - // Load distribution. - balances, err := loadGenesisBalances(genesisBalancesFile) + // Load balances files + balances, err := gnoland.LoadGenesisBalancesFile(c.genesisBalancesFile) if err != nil { - return nil, fmt.Errorf("unable to load genesis balances, %w", err) + return fmt.Errorf("unable to load genesis balances file %q: %w", c.genesisBalancesFile, err) } - // Load initial packages from examples. + // Load examples folder + examplesDir := filepath.Join(c.gnoRootDir, "examples") test1 := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - txs := []std.Tx{} - - // List initial packages to load from examples. - pkgs, err := gnomod.ListPkgs(filepath.Join("..", "examples")) + defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, test1, defaultFee, nil) if err != nil { - panic(fmt.Errorf("listing gno packages: %w", err)) + return fmt.Errorf("unable to load examples folder: %w", err) } - // Sort packages by dependencies. - sortedPkgs, err := pkgs.Sort() + // Load Genesis TXs + genesisTxs, err := gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote) if err != nil { - panic(fmt.Errorf("sorting packages: %w", err)) + return fmt.Errorf("unable to load genesis txs file: %w", err) } - // Filter out draft packages. - nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() - - for _, pkg := range nonDraftPkgs { - // open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) - - var tx std.Tx - tx.Msgs = []std.Msg{ - vmm.MsgAddPackage{ - Creator: test1, - Package: memPkg, - Deposit: nil, - }, - } - tx.Fee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) - } + genesisTxs = append(pkgsTxs, genesisTxs...) - // load genesis txs from file. - txs = append(txs, genesisTxs...) - - // construct genesis AppState. + // Construct genesis AppState. gen.AppState = gnoland.GnoGenesisState{ Balances: balances, - Txs: txs, - } - return gen, nil -} - -func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { - err := gen.SaveAs(filePath) - if err != nil { - panic(err) - } -} - -func loadGenesisTxs( - path string, - chainID string, - genesisRemote string, -) ([]std.Tx, error) { - txs := make([]std.Tx, 0) - - if !osm.FileExists(path) { - // No initial transactions - return txs, nil - } - - txsFile, openErr := os.Open(path) - if openErr != nil { - return nil, fmt.Errorf("unable to open genesis txs file, %w", openErr) - } - - scanner := bufio.NewScanner(txsFile) - - for scanner.Scan() { - txLine := scanner.Text() - - if txLine == "" { - continue // skip empty line - } - - // patch the TX - txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) - txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) - - var tx std.Tx - - if unmarshalErr := amino.UnmarshalJSON([]byte(txLine), &tx); unmarshalErr != nil { - return nil, fmt.Errorf("unable to amino unmarshal tx, %w", unmarshalErr) - } - - txs = append(txs, tx) + Txs: genesisTxs, } - if scanErr := scanner.Err(); scanErr != nil { - return nil, fmt.Errorf("error encountered while scanning, %w", scanErr) + // Write genesis state + if err := gen.SaveAs(genesisFile); err != nil { + return fmt.Errorf("unable to write genesis file %q: %w", genesisFile, err) } - return txs, nil + return nil } -func loadGenesisBalances(path string) ([]string, error) { - // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot - balances := make([]string, 0) - - if !osm.FileExists(path) { - // No initial balances - return balances, nil - } - - balancesFile, openErr := os.Open(path) - if openErr != nil { - return nil, fmt.Errorf("unable to open genesis balances file, %w", openErr) - } - - scanner := bufio.NewScanner(balancesFile) - - for scanner.Scan() { - line := scanner.Text() - - line = strings.TrimSpace(line) - - // remove comments. - line = strings.Split(line, "#")[0] - line = strings.TrimSpace(line) +// getTxEventStoreConfig constructs an event store config from provided user options +func getTxEventStoreConfig(c *startCfg) (*eventstorecfg.Config, error) { + var cfg *eventstorecfg.Config - // skip empty lines. - if line == "" { - continue + switch c.txEventStoreType { + case file.EventStoreType: + if c.txEventStorePath == "" { + return nil, errors.New("unspecified file transaction indexer path") } - if len(strings.Split(line, "=")) != 2 { - return nil, fmt.Errorf("invalid genesis_balance line: %s", line) + // Fill out the configuration + cfg = &eventstorecfg.Config{ + EventStoreType: file.EventStoreType, + Params: map[string]any{ + file.Path: c.txEventStorePath, + }, } - - balances = append(balances, line) - } - - if scanErr := scanner.Err(); scanErr != nil { - return nil, fmt.Errorf("error encountered while scanning, %w", scanErr) + default: + cfg = eventstorecfg.DefaultEventStoreConfig() } - return balances, nil + return cfg, nil } diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar index 5e871b058ac..5f1ee0caf49 100644 --- a/gno.land/cmd/gnoland/testdata/addpkg.txtar +++ b/gno.land/cmd/gnoland/testdata/addpkg.txtar @@ -10,7 +10,11 @@ gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 10000 gnokey maketx call -pkgpath gno.land/r/foobar/bar -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 ## compare render -cmp stdout stdout.golden +stdout '("hello from foo" string)' +stdout 'OK!' +stdout 'GAS WANTED: 2000000' +stdout 'GAS USED: [0-9]+' + -- bar.gno -- package bar @@ -19,8 +23,3 @@ func Render(path string) string { return "hello from foo" } --- stdout.golden -- -("hello from foo" string) -OK! -GAS WANTED: 2000000 -GAS USED: 69163 \ No newline at end of file diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 0d9398cb8e2..b080e0b403d 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -486,11 +486,11 @@ func writeError(w http.ResponseWriter, err error) { // XXX: writeError should return an error page template. w.WriteHeader(500) - details := errors.Unwrap(err).Error() - main := err.Error() + fmt.Println("main", err.Error()) - fmt.Println("main", main) - fmt.Println("details", details) + if details := errors.Unwrap(err); details != nil { + fmt.Println("details", details.Error()) + } w.Write([]byte(err.Error())) } diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 974d3f987b7..61650563405 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -4,10 +4,12 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "strings" "testing" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/tm2/pkg/log" "github.com/gotuna/gotuna/test/assert" ) @@ -41,20 +43,17 @@ func TestRoutes(t *testing.T) { {"/blog", found, "/r/gnoland/blog"}, {"/404-not-found", notFound, "/404-not-found"}, } - if wd, err := os.Getwd(); err == nil { - if strings.HasSuffix(wd, "cmd/gnoweb") { - os.Chdir("../..") - } - } else { - panic("os.Getwd() -> err: " + err.Error()) - } - // configure default values - flags.RemoteAddr = "127.0.0.1:26657" - flags.HelpRemote = "127.0.0.1:26657" + config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) + defer node.Stop() + + // set the `remoteAddr` of the client to the listening address of the + // node, which is randomly assigned. + flags.RemoteAddr = remoteAddr flags.HelpChainID = "dev" flags.CaptchaSite = "" - flags.ViewsDir = "./cmd/gnoweb/views" + flags.ViewsDir = "../../cmd/gnoweb/views" flags.WithAnalytics = false app := makeApp() @@ -93,27 +92,34 @@ func TestAnalytics(t *testing.T) { "/404-not-found", } + config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) + defer node.Stop() + + flags.ViewsDir = "../../cmd/gnoweb/views" t.Run("with", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { + flags.RemoteAddr = remoteAddr flags.WithAnalytics = true app := makeApp() request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) - assert.Contains(t, response.Body.String(), "simpleanalytics") + assert.Contains(t, response.Body.String(), "sa.gno.services") }) } }) t.Run("without", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { + flags.RemoteAddr = remoteAddr flags.WithAnalytics = false app := makeApp() request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) - assert.Equal(t, strings.Contains(response.Body.String(), "simpleanalytics"), false) + assert.Equal(t, strings.Contains(response.Body.String(), "sa.gno.services"), false) }) } }) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 3585f99d7de..a8a2736c8d1 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -1,10 +1,12 @@ package gnoland import ( + "errors" "fmt" "os" "os/exec" "path/filepath" + "runtime" "strings" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -36,7 +38,7 @@ func NewAppOptions() *AppOptions { return &AppOptions{ Logger: log.NewNopLogger(), DB: dbm.NewMemDB(), - GnoRootDir: GuessGnoRootDir(), + GnoRootDir: MustGuessGnoRootDir(), } } @@ -73,6 +75,8 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) + + // XXX: Embed this ? stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) @@ -142,10 +146,9 @@ func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank genState := req.AppState.(GnoGenesisState) // Parse and set genesis state balances. for _, bal := range genState.Balances { - addr, coins := parseBalance(bal) - acc := acctKpr.NewAccountWithAddress(ctx, addr) + acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) acctKpr.SetAccount(ctx, acc) - err := bankKpr.SetCoins(ctx, addr, coins) + err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) if err != nil { panic(err) } @@ -195,24 +198,44 @@ func EndBlocker(vmk vm.VMKeeperI) func(ctx sdk.Context, req abci.RequestEndBlock } } -func GuessGnoRootDir() string { - var rootdir string +// XXX: all the method bellow should be removed in favor of +// https://github.com/gnolang/gno/pull/1233 +func MustGuessGnoRootDir() string { + root, err := GuessGnoRootDir() + if err != nil { + panic(err) + } + + return root +} +func GuessGnoRootDir() (string, error) { // First try to get the root directory from the GNOROOT environment variable. - if rootdir = os.Getenv("GNOROOT"); rootdir != "" { - return filepath.Clean(rootdir) + if rootdir := os.Getenv("GNOROOT"); rootdir != "" { + return filepath.Clean(rootdir), nil } + // Try to guess GNOROOT using the nearest go.mod. if gobin, err := exec.LookPath("go"); err == nil { // If GNOROOT is not set, try to guess the root directory using the `go list` command. cmd := exec.Command(gobin, "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") out, err := cmd.CombinedOutput() - if err != nil { - panic(fmt.Errorf("invalid gno directory %q: %w", rootdir, err)) + if err == nil { + return strings.TrimSpace(string(out)), nil } + } - return strings.TrimSpace(string(out)) + // Try to guess GNOROOT using caller stack. + if _, filename, _, ok := runtime.Caller(1); ok && filepath.IsAbs(filename) { + if currentDir := filepath.Dir(filename); currentDir != "" { + // Gno root directory relative from `app.go` path: + // gno/ .. /gno.land/ .. /pkg/ .. /gnoland/app.go + rootdir, err := filepath.Abs(filepath.Join(currentDir, "..", "..", "..")) + if err == nil { + return rootdir, nil + } + } } - panic("no go binary available, unable to determine gno root-dir path") + return "", errors.New("unable to guess gno's root-directory") } diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go new file mode 100644 index 00000000000..e809103469d --- /dev/null +++ b/gno.land/pkg/gnoland/genesis.go @@ -0,0 +1,126 @@ +package gnoland + +import ( + "errors" + "fmt" + "strings" + + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/amino" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// LoadGenesisBalancesFile loads genesis balances from the provided file path. +func LoadGenesisBalancesFile(path string) ([]Balance, error) { + // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot + content := osm.MustReadFile(path) + lines := strings.Split(string(content), "\n") + + balances := make([]Balance, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + + // remove comments. + line = strings.Split(line, "#")[0] + line = strings.TrimSpace(line) + + // skip empty lines. + if line == "" { + continue + } + + parts := strings.Split(line, "=") //
= + if len(parts) != 2 { + return nil, errors.New("invalid genesis_balance line: " + line) + } + + addr, err := crypto.AddressFromBech32(parts[0]) + if err != nil { + return nil, fmt.Errorf("invalid balance addr %s: %w", parts[0], err) + } + + coins, err := std.ParseCoins(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid balance coins %s: %w", parts[1], err) + } + + balances = append(balances, Balance{ + Address: addr, + Amount: coins, + }) + } + + return balances, nil +} + +// LoadGenesisTxsFile loads genesis transactions from the provided file path. +// XXX: Improve the way we generate and load this file +func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]std.Tx, error) { + txs := []std.Tx{} + txsBz := osm.MustReadFile(path) + txsLines := strings.Split(string(txsBz), "\n") + for _, txLine := range txsLines { + if txLine == "" { + continue // Skip empty line. + } + + // Patch the TX. + txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) + txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) + + var tx std.Tx + if err := amino.UnmarshalJSON([]byte(txLine), &tx); err != nil { + return nil, fmt.Errorf("unable to Unmarshall txs file: %w", err) + } + + txs = append(txs, tx) + } + + return txs, nil +} + +// LoadPackagesFromDir loads gno packages from a directory. +// It creates and returns a list of transactions based on these packages. +func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { + // list all packages from target path + pkgs, err := gnomod.ListPkgs(dir) + if err != nil { + return nil, fmt.Errorf("listing gno packages: %w", err) + } + + // Sort packages by dependencies. + sortedPkgs, err := pkgs.Sort() + if err != nil { + return nil, fmt.Errorf("sorting packages: %w", err) + } + + // Filter out draft packages. + nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() + txs := []std.Tx{} + for _, pkg := range nonDraftPkgs { + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + + // Create transaction + tx := std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: creator, + Package: memPkg, + Deposit: deposit, + }, + }, + } + + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs = append(txs, tx) + } + + return txs, nil +} diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go new file mode 100644 index 00000000000..a0ab6a51e82 --- /dev/null +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -0,0 +1,147 @@ +package gnoland + +import ( + "fmt" + "time" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/bft/proxy" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type InMemoryNodeConfig struct { + PrivValidator bft.PrivValidator // identity of the validator + Genesis *bft.GenesisDoc + TMConfig *tmcfg.Config + SkipFailingGenesisTxs bool + GenesisMaxVMCycles int64 +} + +// NewMockedPrivValidator generate a new key +func NewMockedPrivValidator() bft.PrivValidator { + return bft.NewMockPVWithParams(ed25519.GenPrivKey(), false, false) +} + +// NewInMemoryNodeConfig creates a default configuration for an in-memory node. +func NewDefaultGenesisConfig(pk crypto.PubKey, chainid string) *bft.GenesisDoc { + return &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: chainid, + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 10_0000_000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + }, + AppState: &GnoGenesisState{ + Balances: []Balance{}, + Txs: []std.Tx{}, + }, + } +} + +func NewDefaultTMConfig(rootdir string) *tmcfg.Config { + return tmcfg.DefaultConfig().SetRootDir(rootdir) +} + +// NewInMemoryNodeConfig creates a default configuration for an in-memory node. +func NewDefaultInMemoryNodeConfig(rootdir string) *InMemoryNodeConfig { + tm := NewDefaultTMConfig(rootdir) + + // Create Mocked Identity + pv := NewMockedPrivValidator() + genesis := NewDefaultGenesisConfig(pv.GetPubKey(), tm.ChainID()) + + // Add self as validator + self := pv.GetPubKey() + genesis.Validators = []bft.GenesisValidator{ + { + Address: self.Address(), + PubKey: self, + Power: 10, + Name: "self", + }, + } + + return &InMemoryNodeConfig{ + PrivValidator: pv, + TMConfig: tm, + Genesis: genesis, + GenesisMaxVMCycles: 10_000_000, + } +} + +func (cfg *InMemoryNodeConfig) validate() error { + if cfg.PrivValidator == nil { + return fmt.Errorf("`PrivValidator` is required but not provided") + } + + if cfg.TMConfig == nil { + return fmt.Errorf("`TMConfig` is required but not provided") + } + + if cfg.TMConfig.RootDir == "" { + return fmt.Errorf("`TMConfig.RootDir` is required to locate `stdlibs` directory") + } + + return nil +} + +// NewInMemoryNode creates an in-memory gnoland node. In this mode, the node does not +// persist any data and uses an in-memory database. The `InMemoryNodeConfig.TMConfig.RootDir` +// should point to the correct gno repository to load the stdlibs. +func NewInMemoryNode(logger log.Logger, cfg *InMemoryNodeConfig) (*node.Node, error) { + if err := cfg.validate(); err != nil { + return nil, fmt.Errorf("validate config error: %w", err) + } + + // Initialize the application with the provided options + gnoApp, err := NewAppWithOptions(&AppOptions{ + Logger: logger, + GnoRootDir: cfg.TMConfig.RootDir, + SkipFailingGenesisTxs: cfg.SkipFailingGenesisTxs, + MaxCycles: cfg.GenesisMaxVMCycles, + DB: db.NewMemDB(), + }) + if err != nil { + return nil, fmt.Errorf("error initializing new app: %w", err) + } + + cfg.TMConfig.LocalApp = gnoApp + + // Setup app client creator + appClientCreator := proxy.DefaultClientCreator( + cfg.TMConfig.LocalApp, + cfg.TMConfig.ProxyApp, + cfg.TMConfig.ABCI, + cfg.TMConfig.DBDir(), + ) + + // Create genesis factory + genProvider := func() (*bft.GenesisDoc, error) { + return cfg.Genesis, nil + } + + // generate p2p node identity + // XXX: do we need to configur + nodekey := &p2p.NodeKey{PrivKey: ed25519.GenPrivKey()} + + // Create and return the in-memory node instance + return node.NewNode(cfg.TMConfig, + cfg.PrivValidator, nodekey, + appClientCreator, + genProvider, + node.DefaultDBProvider, + logger, + ) +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 1c762366ae9..5d68064c9c5 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,9 +1,20 @@ package gnoland import ( + "errors" + "fmt" + "strings" + + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) +var ( + ErrBalanceEmptyAddress = errors.New("balance address is empty") + ErrBalanceEmptyAmount = errors.New("balance amount is empty") +) + type GnoAccount struct { std.BaseAccount } @@ -13,6 +24,56 @@ func ProtoGnoAccount() std.Account { } type GnoGenesisState struct { - Balances []string `json:"balances"` - Txs []std.Tx `json:"txs"` + Balances []Balance `json:"balances"` + Txs []std.Tx `json:"txs"` +} + +type Balance struct { + Address bft.Address + Amount std.Coins +} + +func (b *Balance) Verify() error { + if b.Address.IsZero() { + return ErrBalanceEmptyAddress + } + + if b.Amount.Len() == 0 { + return ErrBalanceEmptyAmount + } + + return nil +} + +func (b *Balance) Parse(entry string) error { + parts := strings.Split(strings.TrimSpace(entry), "=") //
= + if len(parts) != 2 { + return fmt.Errorf("malformed entry: %q", entry) + } + + var err error + + b.Address, err = crypto.AddressFromBech32(parts[0]) + if err != nil { + return fmt.Errorf("invalid address %q: %w", parts[0], err) + } + + b.Amount, err = std.ParseCoins(parts[1]) + if err != nil { + return fmt.Errorf("invalid amount %q: %w", parts[1], err) + } + + return nil +} + +func (b *Balance) UnmarshalAmino(rep string) error { + return b.Parse(rep) +} + +func (b Balance) MarshalAmino() (string, error) { + return b.String(), nil +} + +func (b Balance) String() string { + return fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String()) } diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go new file mode 100644 index 00000000000..97222d0cdfd --- /dev/null +++ b/gno.land/pkg/gnoland/types_test.go @@ -0,0 +1,98 @@ +package gnoland + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/jaekwon/testify/assert" + "github.com/jaekwon/testify/require" +) + +func TestBalance_Verify(t *testing.T) { + validAddress := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + emptyAmount := std.Coins{} + nonEmptyAmount := std.NewCoins(std.NewCoin("test", 100)) + + tests := []struct { + name string + balance Balance + expectErr bool + }{ + {"empty amount", Balance{Address: validAddress, Amount: emptyAmount}, true}, + {"empty address", Balance{Address: bft.Address{}, Amount: nonEmptyAmount}, true}, + {"valid balance", Balance{Address: validAddress, Amount: nonEmptyAmount}, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.balance.Verify() + if tc.expectErr { + assert.Error(t, err, fmt.Sprintf("TestVerifyBalance: %s", tc.name)) + } else { + assert.NoError(t, err, fmt.Sprintf("TestVerifyBalance: %s", tc.name)) + } + }) + } +} + +func TestBalance_Parse(t *testing.T) { + validAddress := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + validBalance := Balance{Address: validAddress, Amount: std.NewCoins(std.NewCoin("test", 100))} + + tests := []struct { + name string + entry string + expected Balance + expectErr bool + }{ + {"valid entry", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=100test", validBalance, false}, + {"invalid address", "invalid=100test", Balance{}, true}, + {"incomplete entry", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Balance{}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + balance := Balance{} + err := balance.Parse(tc.entry) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, balance) + } + }) + } +} + +func TestBalance_AminoUnmarshalJSON(t *testing.T) { + expected := Balance{ + Address: crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + Amount: std.MustParseCoins("100ugnot"), + } + value := fmt.Sprintf("[%q]", expected.String()) + + var balances []Balance + err := amino.UnmarshalJSON([]byte(value), &balances) + require.NoError(t, err) + require.Len(t, balances, 1, "there should be one balance after unmarshaling") + + balance := balances[0] + require.Equal(t, expected.Address, balance.Address) + require.True(t, expected.Amount.IsEqual(balance.Amount)) +} + +func TestBalance_AminoMarshalJSON(t *testing.T) { + expected := Balance{ + Address: crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + Amount: std.MustParseCoins("100ugnot"), + } + expectedJSON := fmt.Sprintf("[%q]", expected.String()) + + balancesJSON, err := amino.MarshalJSON([]Balance{expected}) + require.NoError(t, err) + require.JSONEq(t, expectedJSON, string(balancesJSON)) +} diff --git a/gno.land/pkg/integration/gnoland.go b/gno.land/pkg/integration/gnoland.go deleted file mode 100644 index 318d76eea86..00000000000 --- a/gno.land/pkg/integration/gnoland.go +++ /dev/null @@ -1,334 +0,0 @@ -package integration - -import ( - "flag" - "fmt" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/gnomod" - "github.com/gnolang/gno/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/config" - "github.com/gnolang/gno/tm2/pkg/bft/node" - "github.com/gnolang/gno/tm2/pkg/bft/privval" - bft "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/db" - "github.com/gnolang/gno/tm2/pkg/log" - osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/rogpeppe/go-internal/testscript" -) - -type IntegrationConfig struct { - SkipFailingGenesisTxs bool - SkipStart bool - GenesisBalancesFile string - GenesisTxsFile string - ChainID string - GenesisRemote string - RootDir string - GenesisMaxVMCycles int64 - Config string -} - -// NOTE: This is a copy of gnoland actual flags. -// XXX: A lot this make no sense for integration. -func (c *IntegrationConfig) RegisterFlags(fs *flag.FlagSet) { - fs.BoolVar( - &c.SkipFailingGenesisTxs, - "skip-failing-genesis-txs", - false, - "don't panic when replaying invalid genesis txs", - ) - fs.BoolVar( - &c.SkipStart, - "skip-start", - false, - "quit after initialization, don't start the node", - ) - - fs.StringVar( - &c.GenesisBalancesFile, - "genesis-balances-file", - "./genesis/genesis_balances.txt", - "initial distribution file", - ) - - fs.StringVar( - &c.GenesisTxsFile, - "genesis-txs-file", - "./genesis/genesis_txs.txt", - "initial txs to replay", - ) - - fs.StringVar( - &c.ChainID, - "chainid", - "dev", - "the ID of the chain", - ) - - fs.StringVar( - &c.RootDir, - "root-dir", - "testdir", - "directory for config and data", - ) - - fs.StringVar( - &c.GenesisRemote, - "genesis-remote", - "localhost:26657", - "replacement for '%%REMOTE%%' in genesis", - ) - - fs.Int64Var( - &c.GenesisMaxVMCycles, - "genesis-max-vm-cycles", - 10_000_000, - "set maximum allowed vm cycles per operation. Zero means no limit.", - ) -} - -func execTestingGnoland(t *testing.T, logger log.Logger, gnoDataDir, gnoRootDir string, args []string) (*node.Node, error) { - t.Helper() - - // Setup start config. - icfg := &IntegrationConfig{} - { - fs := flag.NewFlagSet("start", flag.ExitOnError) - icfg.RegisterFlags(fs) - - // Override default value for flags. - fs.VisitAll(func(f *flag.Flag) { - switch f.Name { - case "root-dir": - f.DefValue = gnoDataDir - case "chainid": - f.DefValue = "tendermint_test" - case "genesis-balances-file": - f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_balances.txt") - case "genesis-txs-file": - f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_txs.txt") - default: - return - } - - f.Value.Set(f.DefValue) - }) - - if err := fs.Parse(args); err != nil { - return nil, fmt.Errorf("unable to parse flags: %w", err) - } - } - - // Setup testing config. - cfg := config.TestConfig().SetRootDir(gnoDataDir) - { - cfg.EnsureDirs() - cfg.RPC.ListenAddress = "tcp://127.0.0.1:0" - cfg.P2P.ListenAddress = "tcp://127.0.0.1:0" - } - - // Prepare genesis. - if err := setupTestingGenesis(gnoDataDir, cfg, icfg, gnoRootDir); err != nil { - return nil, err - } - - // Create application and node. - return createAppAndNode(cfg, logger, gnoRootDir, icfg) -} - -func setupTestingGenesis(gnoDataDir string, cfg *config.Config, icfg *IntegrationConfig, gnoRootDir string) error { - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() - priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) - - genesisFilePath := filepath.Join(gnoDataDir, cfg.Genesis) - genesisDirPath := filepath.Dir(genesisFilePath) - if err := osm.EnsureDir(genesisDirPath, 0o700); err != nil { - return fmt.Errorf("unable to ensure directory %q: %w", genesisDirPath, err) - } - - genesisTxs := loadGenesisTxs(icfg.GenesisTxsFile, icfg.ChainID, icfg.GenesisRemote) - pvPub := priv.GetPubKey() - - gen := &bft.GenesisDoc{ - GenesisTime: time.Now(), - ChainID: icfg.ChainID, - ConsensusParams: abci.ConsensusParams{ - Block: &abci.BlockParams{ - // TODO: update limits. - MaxTxBytes: 1000000, // 1MB, - MaxDataBytes: 2000000, // 2MB, - MaxGas: 10000000, // 10M gas - TimeIotaMS: 100, // 100ms - }, - }, - Validators: []bft.GenesisValidator{ - { - Address: pvPub.Address(), - PubKey: pvPub, - Power: 10, - Name: "testvalidator", - }, - }, - } - - // Load distribution. - balances := loadGenesisBalances(icfg.GenesisBalancesFile) - - // Load initial packages from examples. - // XXX: We should be able to config this. - test1 := crypto.MustAddressFromString(test1Addr) - txs := []std.Tx{} - - // List initial packages to load from examples. - // println(filepath.Join(gnoRootDir, "examples")) - pkgs, err := gnomod.ListPkgs(filepath.Join(gnoRootDir, "examples")) - if err != nil { - return fmt.Errorf("listing gno packages: %w", err) - } - - // Sort packages by dependencies. - sortedPkgs, err := pkgs.Sort() - if err != nil { - return fmt.Errorf("sorting packages: %w", err) - } - - // Filter out draft packages. - nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() - - for _, pkg := range nonDraftPkgs { - // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) - - var tx std.Tx - tx.Msgs = []std.Msg{ - vmm.MsgAddPackage{ - Creator: test1, - Package: memPkg, - Deposit: nil, - }, - } - - // XXX: Add fee flag ? - // Or maybe reduce fee to the minimum ? - tx.Fee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) - } - - // Load genesis txs from file. - txs = append(txs, genesisTxs...) - - // Construct genesis AppState. - gen.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: txs, - } - - writeGenesisFile(gen, genesisFilePath) - - return nil -} - -func createAppAndNode(cfg *config.Config, logger log.Logger, gnoRootDir string, icfg *IntegrationConfig) (*node.Node, error) { - gnoApp, err := gnoland.NewAppWithOptions(&gnoland.AppOptions{ - Logger: logger, - GnoRootDir: gnoRootDir, - SkipFailingGenesisTxs: icfg.SkipFailingGenesisTxs, - MaxCycles: icfg.GenesisMaxVMCycles, - DB: db.NewMemDB(), - }) - if err != nil { - return nil, fmt.Errorf("error in creating new app: %w", err) - } - - cfg.LocalApp = gnoApp - node, err := node.DefaultNewNode(cfg, logger) - if err != nil { - return nil, fmt.Errorf("error in creating node: %w", err) - } - - return node, node.Start() -} - -func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { - if err != nil { - fmt.Fprintf(ts.Stderr(), "%q error: %v\n", cmd, err) - if !neg { - ts.Fatalf("unexpected %q command failure: %s", cmd, err) - } - } else { - if neg { - ts.Fatalf("unexpected %s command success", cmd) - } - } -} - -func loadGenesisTxs( - path string, - chainID string, - genesisRemote string, -) []std.Tx { - txs := []std.Tx{} - txsBz := osm.MustReadFile(path) - txsLines := strings.Split(string(txsBz), "\n") - for _, txLine := range txsLines { - if txLine == "" { - continue // Skip empty line. - } - - // Patch the TX. - txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) - txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) - - var tx std.Tx - amino.MustUnmarshalJSON([]byte(txLine), &tx) - txs = append(txs, tx) - } - - return txs -} - -func loadGenesisBalances(path string) []string { - // Each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot. - balances := []string{} - content := osm.MustReadFile(path) - lines := strings.Split(string(content), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - - // Remove comments. - line = strings.Split(line, "#")[0] - line = strings.TrimSpace(line) - - // Skip empty lines. - if line == "" { - continue - } - - parts := strings.Split(line, "=") - if len(parts) != 2 { - panic("invalid genesis_balance line: " + line) - } - - balances = append(balances, line) - } - return balances -} - -func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { - err := gen.SaveAs(filePath) - if err != nil { - panic(err) - } -} diff --git a/gno.land/pkg/integration/testing.go b/gno.land/pkg/integration/testing.go new file mode 100644 index 00000000000..7803e213da1 --- /dev/null +++ b/gno.land/pkg/integration/testing.go @@ -0,0 +1,39 @@ +package integration + +import ( + "errors" + + "github.com/jaekwon/testify/assert" + "github.com/jaekwon/testify/require" + "github.com/rogpeppe/go-internal/testscript" +) + +// This error is from testscript.Fatalf and is needed to correctly +// handle the FailNow method. +// see: https://github.com/rogpeppe/go-internal/blob/32ae33786eccde1672d4ba373c80e1bc282bfbf6/testscript/testscript.go#L799-L812 +var errFailNow = errors.New("fail now!") //nolint:stylecheck + +var ( + _ require.TestingT = (*testingTS)(nil) + _ assert.TestingT = (*testingTS)(nil) +) + +type TestingTS = require.TestingT + +type testingTS struct { + *testscript.TestScript +} + +func TSTestingT(ts *testscript.TestScript) TestingTS { + return &testingTS{ts} +} + +func (t *testingTS) Errorf(format string, args ...interface{}) { + defer recover() // we can ignore recover result, we just want to catch it up + t.Fatalf(format, args...) +} + +func (t *testingTS) FailNow() { + // unfortunately we can't access underlying `t.t.FailNow` method + panic(errFailNow) +} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index f0a696ddd85..b773317513f 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -10,28 +10,18 @@ import ( "strings" "sync" "testing" - "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/node" - "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" - "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/rogpeppe/go-internal/testscript" ) -// XXX: This should be centralize somewhere. -const ( - test1Addr = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" - test1Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" -) - type testNode struct { *node.Node - logger log.Logger nGnoKeyExec uint // Counter for execution of gnokey. } @@ -51,15 +41,11 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // `gnoRootDir` should point to the local location of the gno repository. // It serves as the gno equivalent of GOROOT. - gnoRootDir := gnoland.GuessGnoRootDir() + gnoRootDir := gnoland.MustGuessGnoRootDir() // `gnoHomeDir` should be the local directory where gnokey stores keys. gnoHomeDir := filepath.Join(tmpdir, "gno") - // `gnoDataDir` should refer to the local location where the gnoland node - // stores its configuration and data. - gnoDataDir := filepath.Join(tmpdir, "data") - // Testscripts run concurrently by default, so we need to be prepared for that. var muNodes sync.Mutex nodes := map[string]*testNode{} @@ -76,10 +62,35 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { return err } - // XXX: Add a command to add custom account. - kb.CreateAccount("test1", test1Seed, "", "", 0, 0) - env.Setenv("USER_SEED_test1", test1Seed) - env.Setenv("USER_ADDR_test1", test1Addr) + // create sessions ID + var sid string + { + works := env.Getenv("WORK") + sum := crc32.ChecksumIEEE([]byte(works)) + sid = strconv.FormatUint(uint64(sum), 16) + env.Setenv("SID", sid) + } + + // setup logger + var logger log.Logger + { + logger = log.NewNopLogger() + if persistWorkDir || os.Getenv("LOG_DIR") != "" { + logname := fmt.Sprintf("gnoland-%s.log", sid) + logger, err = getTestingLogger(env, logname) + if err != nil { + return fmt.Errorf("unable to setup logger: %w", err) + } + } + + env.Values["_logger"] = logger + } + + // Setup "test1" default account + kb.CreateAccount(DefaultAccount_Name, DefaultAccount_Seed, "", "", 0, 0) + + env.Setenv("USER_SEED_"+DefaultAccount_Name, DefaultAccount_Seed) + env.Setenv("USER_ADDR_"+DefaultAccount_Name, DefaultAccount_Address) env.Setenv("GNOROOT", gnoRootDir) env.Setenv("GNOHOME", gnoHomeDir) @@ -96,7 +107,8 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { return } - sid := getSessionID(ts) + logger := ts.Value("_logger").(log.Logger) // grab logger + sid := ts.Getenv("SID") // grab session id var cmd string cmd, args = args[0], args[1:] @@ -109,63 +121,20 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { break } - logger := log.NewNopLogger() - if persistWorkDir || os.Getenv("LOG_DIR") != "" { - logname := fmt.Sprintf("gnoland-%s.log", sid) - logger = getTestingLogger(ts, logname) - } + // Warp up `ts` so we can pass it to other testing method + t := TSTestingT(ts) - dataDir := filepath.Join(gnoDataDir, sid) - var node *node.Node - if node, err = execTestingGnoland(t, logger, dataDir, gnoRootDir, args); err == nil { - nodes[sid] = &testNode{ - Node: node, - logger: logger, - } - ts.Defer(func() { - muNodes.Lock() - defer muNodes.Unlock() - - if n := nodes[sid]; n != nil { - if err := n.Stop(); err != nil { - panic(fmt.Errorf("node %q was unable to stop: %w", sid, err)) - } - } - }) - - // Get listen address environment. - // It should have been updated with the right port on start. - laddr := node.Config().RPC.ListenAddress - - // Add default environements. - ts.Setenv("RPC_ADDR", laddr) - ts.Setenv("GNODATA", gnoDataDir) - - const listenerID = "testing_listener" - - // Wait for first block by waiting for `EventNewBlock` event. - nb := make(chan struct{}, 1) - node.EventSwitch().AddListener(listenerID, func(ev events.Event) { - if _, ok := ev.(types.EventNewBlock); ok { - select { - case nb <- struct{}{}: - default: - } - } - }) - - if node.BlockStore().Height() == 0 { - select { - case <-nb: // ok - case <-time.After(time.Second * 6): - ts.Fatalf("timeout while waiting for the node to start") - } - } - - node.EventSwitch().RemoveListener(listenerID) - - fmt.Fprintln(ts.Stdout(), "node started successfully") - } + // Generate config and node + cfg := TestingMinimalNodeConfig(t, gnoRootDir) + n, remoteAddr := TestingInMemoryNode(t, logger, cfg) + + // Register cleanup + nodes[sid] = &testNode{Node: n} + + // Add default environements + ts.Setenv("RPC_ADDR", remoteAddr) + + fmt.Fprintln(ts.Stdout(), "node started successfully") case "stop": n, ok := nodes[sid] if !ok { @@ -176,9 +145,8 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { if err = n.Stop(); err == nil { delete(nodes, sid) - // Unset gnoland environements. + // Unset gnoland environements ts.Setenv("RPC_ADDR", "") - ts.Setenv("GNODATA", "") fmt.Fprintln(ts.Stdout(), "node stopped successfully") } default: @@ -191,9 +159,10 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { muNodes.Lock() defer muNodes.Unlock() - sid := getSessionID(ts) + logger := ts.Value("_logger").(log.Logger) // grab logger + sid := ts.Getenv("SID") // grab session id - // Setup IO command. + // Setup IO command io := commands.NewTestIO() io.SetOut(commands.WriteNopCloser(ts.Stdout())) io.SetErr(commands.WriteNopCloser(ts.Stderr())) @@ -212,9 +181,10 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { n.nGnoKeyExec++ headerlog := fmt.Sprintf("%.02d!EXEC_GNOKEY", n.nGnoKeyExec) + // Log the command inside gnoland logger, so we can better scope errors. - n.logger.Info(headerlog, strings.Join(args, " ")) - defer n.logger.Info(headerlog, "END") + logger.Info(headerlog, strings.Join(args, " ")) + defer logger.Info(headerlog, "END") } // Inject default argument, if duplicate @@ -230,35 +200,30 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { } } -func getSessionID(ts *testscript.TestScript) string { - works := ts.Getenv("WORK") - sum := crc32.ChecksumIEEE([]byte(works)) - return strconv.FormatUint(uint64(sum), 16) -} - -func getTestingLogger(ts *testscript.TestScript, logname string) log.Logger { +func getTestingLogger(env *testscript.Env, logname string) (log.Logger, error) { var path string + if logdir := os.Getenv("LOG_DIR"); logdir != "" { if err := os.MkdirAll(logdir, 0o755); err != nil { - ts.Fatalf("unable to make log directory %q", logdir) + return nil, fmt.Errorf("unable to make log directory %q", logdir) } var err error if path, err = filepath.Abs(filepath.Join(logdir, logname)); err != nil { - ts.Fatalf("uanble to get absolute path of logdir %q", logdir) + return nil, fmt.Errorf("uanble to get absolute path of logdir %q", logdir) } - } else if workdir := ts.Getenv("WORK"); workdir != "" { + } else if workdir := env.Getenv("WORK"); workdir != "" { path = filepath.Join(workdir, logname) } else { - return log.NewNopLogger() + return log.NewNopLogger(), nil } f, err := os.Create(path) if err != nil { - ts.Fatalf("unable to create log file %q: %s", path, err.Error()) + return nil, fmt.Errorf("unable to create log file %q: %w", path, err) } - ts.Defer(func() { + env.Defer(func() { if err := f.Close(); err != nil { panic(fmt.Errorf("unable to close log file %q: %w", path, err)) } @@ -274,9 +239,22 @@ func getTestingLogger(ts *testscript.TestScript, logname string) log.Logger { logger.SetLevel(log.LevelInfo) case "": default: - ts.Fatalf("invalid log level %q", level) + return nil, fmt.Errorf("invalid log level %q", level) } - ts.Logf("starting logger: %q", path) - return logger + env.T().Log("starting logger: %q", path) + return logger, nil +} + +func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { + if err != nil { + fmt.Fprintf(ts.Stderr(), "%q error: %v\n", cmd, err) + if !neg { + ts.Fatalf("unexpected %q command failure: %s", cmd, err) + } + } else { + if neg { + ts.Fatalf("unexpected %q command success", cmd) + } + } } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go new file mode 100644 index 00000000000..1ca7e11eb63 --- /dev/null +++ b/gno.land/pkg/integration/testing_node.go @@ -0,0 +1,184 @@ +package integration + +import ( + "path/filepath" + "sync" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/jaekwon/testify/require" +) + +const ( + DefaultAccount_Name = "test1" + DefaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + DefaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" +) + +// TestingInMemoryNode initializes and starts an in-memory node for testing. +// It returns the node instance and its RPC remote address. +func TestingInMemoryNode(t TestingTS, logger log.Logger, config *gnoland.InMemoryNodeConfig) (*node.Node, string) { + node, err := gnoland.NewInMemoryNode(logger, config) + require.NoError(t, err) + + err = node.Start() + require.NoError(t, err) + + select { + case <-waitForNodeReadiness(node): + case <-time.After(time.Second * 6): + require.FailNow(t, "timeout while waiting for the node to start") + } + + return node, node.Config().RPC.ListenAddress +} + +// TestingNodeConfig constructs an in-memory node configuration +// with default packages and genesis transactions already loaded. +// It will return the default creator address of the loaded packages. +func TestingNodeConfig(t TestingTS, gnoroot string) (*gnoland.InMemoryNodeConfig, bft.Address) { + cfg := TestingMinimalNodeConfig(t, gnoroot) + + creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + + balances := LoadDefaultGenesisBalanceFile(t, gnoroot) + txs := []std.Tx{} + txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) + txs = append(txs, LoadDefaultGenesisTXsFile(t, cfg.Genesis.ChainID, gnoroot)...) + + cfg.Genesis.AppState = gnoland.GnoGenesisState{ + Balances: balances, + Txs: txs, + } + + return cfg, creator +} + +// TestingMinimalNodeConfig constructs the default minimal in-memory node configuration for testing. +func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNodeConfig { + tmconfig := DefaultTestingTMConfig(gnoroot) + + // Create Mocked Identity + pv := gnoland.NewMockedPrivValidator() + + // Generate genesis config + genesis := DefaultTestingGenesisConfig(t, gnoroot, pv.GetPubKey(), tmconfig) + + return &gnoland.InMemoryNodeConfig{ + PrivValidator: pv, + Genesis: genesis, + TMConfig: tmconfig, + } +} + +func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc { + return &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: tmconfig.ChainID(), + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 10_0000_000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + }, + Validators: []bft.GenesisValidator{ + { + Address: self.Address(), + PubKey: self, + Power: 10, + Name: "self", + }, + }, + AppState: gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{ + { + Address: crypto.MustAddressFromString(DefaultAccount_Address), + Amount: std.MustParseCoins("10000000000000ugnot"), + }, + }, + Txs: []std.Tx{}, + }, + } +} + +// LoadDefaultPackages loads the default packages for testing using a given creator address and gnoroot directory. +func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []std.Tx { + examplesDir := filepath.Join(gnoroot, "examples") + + defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + defaultCreator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + txs, err := gnoland.LoadPackagesFromDir(examplesDir, defaultCreator, defaultFee, nil) + require.NoError(t, err) + + return txs +} + +// LoadDefaultGenesisBalanceFile loads the default genesis balance file for testing. +func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balance { + balanceFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") + + genesisBalances, err := gnoland.LoadGenesisBalancesFile(balanceFile) + require.NoError(t, err) + + return genesisBalances +} + +// LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing. +func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []std.Tx { + txsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.txt") + + // NOTE: We dont care about giving a correct address here, as it's only for display + // XXX: Do we care loading this TXs for testing ? + genesisTXs, err := gnoland.LoadGenesisTxsFile(txsFile, chainid, "https://127.0.0.1:26657") + require.NoError(t, err) + + return genesisTXs +} + +// DefaultTestingTMConfig constructs the default Tendermint configuration for testing. +func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config { + const defaultListner = "tcp://127.0.0.1:0" + + tmconfig := tmcfg.TestConfig().SetRootDir(gnoroot) + tmconfig.Consensus.CreateEmptyBlocks = true + tmconfig.Consensus.CreateEmptyBlocksInterval = time.Duration(0) + tmconfig.RPC.ListenAddress = defaultListner + tmconfig.P2P.ListenAddress = defaultListner + return tmconfig +} + +// waitForNodeReadiness waits until the node is ready, signaling via the EventNewBlock event. +// XXX: This should be replace by https://github.com/gnolang/gno/pull/1216 +func waitForNodeReadiness(n *node.Node) <-chan struct{} { + const listenerID = "first_block_listener" + + var once sync.Once + + nb := make(chan struct{}) + ready := func() { + close(nb) + n.EventSwitch().RemoveListener(listenerID) + } + + n.EventSwitch().AddListener(listenerID, func(ev events.Event) { + if _, ok := ev.(bft.EventNewBlock); ok { + once.Do(ready) + } + }) + + if n.BlockStore().Height() > 0 { + once.Do(ready) + } + + return nb +}