diff --git a/contribs/gnogenesis/internal/balances/balances_export.go b/contribs/gnogenesis/internal/balances/balances_export.go index 1970e348b1a..61b4583b15f 100644 --- a/contribs/gnogenesis/internal/balances/balances_export.go +++ b/contribs/gnogenesis/internal/balances/balances_export.go @@ -63,7 +63,9 @@ func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error { defer outputFile.Close() // Save the balances - for _, balance := range state.Balances { + balances := state.Balances + gnoland.SortBalances(balances) + for _, balance := range balances { if _, err = outputFile.WriteString( fmt.Sprintf("%s\n", balance), ); err != nil { diff --git a/contribs/gnogenesis/internal/balances/balances_export_test.go b/contribs/gnogenesis/internal/balances/balances_export_test.go index d4f4723df15..f148b9f4b92 100644 --- a/contribs/gnogenesis/internal/balances/balances_export_test.go +++ b/contribs/gnogenesis/internal/balances/balances_export_test.go @@ -3,11 +3,17 @@ package balances import ( "bufio" "context" + "crypto/md5" + "fmt" + "io" + "os" + "path/filepath" "testing" "github.com/gnolang/contribs/gnogenesis/internal/common" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + bft "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" @@ -105,35 +111,45 @@ func TestGenesis_Balances_Export(t *testing.T) { t.Parallel() // Generate dummy balances - balances := getDummyBalances(t, 10) + balances := getDummyBalances(t, 100) tempGenesis, cleanup := testutils.NewTestFile(t) t.Cleanup(cleanup) - genesis := common.GetDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Balances: balances, - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - // Prepare the output file outputFile, outputCleanup := testutils.NewTestFile(t) t.Cleanup(outputCleanup) - // Create the command - cmd := NewBalancesCmd(commands.NewTestIO()) - args := []string{ - "export", - "--genesis-path", - tempGenesis.Name(), - outputFile.Name(), + saveGenesisFile := func(outputPath string) { + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Balances: balances, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + outputPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) } - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) + saveGenesisFile(outputFile.Name()) + readIt := func(p string) string { + blob, err := os.ReadFile(p) + require.NoError(t, err) + return string(blob) + } // Validate the transactions were written down + outputFile.Seek(0, 0) // Seek back to the front of the outputFile. scanner := bufio.NewScanner(outputFile) outputBalances := make([]gnoland.Balance, 0) @@ -146,11 +162,47 @@ func TestGenesis_Balances_Export(t *testing.T) { } require.NoError(t, scanner.Err()) - assert.Len(t, outputBalances, len(balances)) - for index, balance := range outputBalances { - assert.Equal(t, balances[index], balance) + // Next ensure that all balances are sorted by address, deterministically. + for i := 1; i < len(outputBalances); i++ { + curr := outputBalances[i].Address + for j := 0; j < i; j++ { + prev := outputBalances[j].Address + if addressIsGreater(prev, curr) { + t.Fatalf("Non-deterministic order of exported balances\n\t[%d](%s)\n>\n\t[%d](%s)", j, prev, i, curr) + } + } + } + + // Lastly compute the checksum and ensure that it is the same each of the N times. + outputFile.Close() + firstGenesisPathMD5 := md5SumFromFile(t, outputFile.Name()) + firstGenesis := readIt(outputFile.Name()) + for i := 1; i <= 10; i++ { + outpath := filepath.Join(t.TempDir(), "out") + saveGenesisFile(outpath) + currentMD5 := md5SumFromFile(t, outpath) + currentGenesis := readIt(outpath) + require.Equal(t, firstGenesisPathMD5, currentMD5, "Iteration #%d has a different MD5 checksum\n%s\n\n%s", i, firstGenesis, currentGenesis) } }) } + +func md5SumFromFile(t *testing.T, p string) string { + t.Helper() + + f, err := os.Open(p) + require.NoError(t, err) + fi, err := f.Stat() + require.NoError(t, err) + require.True(t, fi.Size() > 100, "at least 100 bytes expected") + defer f.Close() + h := md5.New() + io.Copy(h, f) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func addressIsGreater(a, b bft.Address) bool { + return a.Compare(b) == 1 +} diff --git a/gno.land/pkg/gnoland/balance.go b/gno.land/pkg/gnoland/balance.go index 807da4cf41f..357e9a7cb6d 100644 --- a/gno.land/pkg/gnoland/balance.go +++ b/gno.land/pkg/gnoland/balance.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "sort" "strings" bft "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -79,14 +80,28 @@ func (bs Balances) Get(address crypto.Address) (balance Balance, ok bool) { return } +// List returns a slice of balances, sorted by Balance.Address +// in lexicographic order. func (bs Balances) List() []Balance { list := make([]Balance, 0, len(bs)) for _, balance := range bs { list = append(list, balance) } + + SortBalances(list) return list } +// Sorts balances in lexicographic order, compared by .Address instead of .Amount +// because .Amount's type is Coins that requires a deeper comparison by .Denom and +// .Amount which are unnecessarily complex yet by the nature of each Balance in Balances, +// each entry will be keyed by the same Address in a map. +func SortBalances(list []Balance) { + sort.Slice(list, func(i, j int) bool { + return list[i].Address.Compare(list[j].Address) == -1 + }) +} + // LeftMerge left-merges the two maps func (bs Balances) LeftMerge(from Balances) { for key, bVal := range from { diff --git a/gno.land/pkg/gnoland/balance_test.go b/gno.land/pkg/gnoland/balance_test.go index 9ee86e5d768..8e77c93948f 100644 --- a/gno.land/pkg/gnoland/balance_test.go +++ b/gno.land/pkg/gnoland/balance_test.go @@ -1,8 +1,11 @@ package gnoland import ( + "bytes" + crand "crypto/rand" "fmt" "math" + "math/rand" "strconv" "strings" "testing" @@ -274,3 +277,24 @@ func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { return secp256k1.PrivKeySecp256k1(derivedPriv) } + +func TestBalancesList(t *testing.T) { + // 1. Generate and insert balances. + balances := NewBalances() + n := 100 + rng := rand.New(rand.NewSource(10)) + for i := 0; i < n; i++ { + var addr bft.Address + crand.Read(addr[:]) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 1+rng.Int63n(100))) + balances.Set(addr, amount) + } + + list := balances.List() + for i := 1; i < len(list); i++ { + for j := 0; j < i; j++ { + isLessOrEqualTo := bytes.Compare(list[j].Address[:], list[i].Address[:]) <= 0 + assert.True(t, isLessOrEqualTo, "Address:\n\t#%d[%x]\n>\n\t#%d[%x]", j, list[j], i, list[i]) + } + } +}