Skip to content

fix(gnogenesis): sort balances before export for deterministic checksum of genesis.json #4128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion contribs/gnogenesis/internal/balances/balances_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 72 additions & 20 deletions contribs/gnogenesis/internal/balances/balances_export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
15 changes: 15 additions & 0 deletions gno.land/pkg/gnoland/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"io"
"sort"
"strings"

bft "github.com/gnolang/gno/tm2/pkg/bft/types"
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 24 additions & 0 deletions gno.land/pkg/gnoland/balance_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package gnoland

import (
"bytes"
crand "crypto/rand"
"fmt"
"math"
"math/rand"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -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])
}
}
}
Loading