Skip to content

Commit a477929

Browse files
committed
fix(gnogenesis): sort balances before export for deterministic checksum of genesis.json
This change sorts balances by lexicographic order to ensure that the checksum produced by the genesis.json is deterministic. Fixes #4122
1 parent 33952b5 commit a477929

File tree

4 files changed

+112
-21
lines changed

4 files changed

+112
-21
lines changed

Diff for: contribs/gnogenesis/internal/balances/balances_export.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error {
6363
defer outputFile.Close()
6464

6565
// Save the balances
66-
for _, balance := range state.Balances {
66+
balances := state.Balances
67+
gnoland.SortBalances(balances)
68+
for _, balance := range balances {
6769
if _, err = outputFile.WriteString(
6870
fmt.Sprintf("%s\n", balance),
6971
); err != nil {

Diff for: contribs/gnogenesis/internal/balances/balances_export_test.go

+71-20
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ package balances
33
import (
44
"bufio"
55
"context"
6+
"crypto/md5"
7+
"fmt"
8+
"io"
9+
"os"
10+
"path/filepath"
611
"testing"
712

813
"github.com/gnolang/contribs/gnogenesis/internal/common"
914
"github.com/gnolang/gno/gno.land/pkg/gnoland"
1015
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
16+
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
1117
"github.com/gnolang/gno/tm2/pkg/commands"
1218
"github.com/gnolang/gno/tm2/pkg/std"
1319
"github.com/gnolang/gno/tm2/pkg/testutils"
@@ -105,35 +111,45 @@ func TestGenesis_Balances_Export(t *testing.T) {
105111
t.Parallel()
106112

107113
// Generate dummy balances
108-
balances := getDummyBalances(t, 10)
114+
balances := getDummyBalances(t, 100)
109115

110116
tempGenesis, cleanup := testutils.NewTestFile(t)
111117
t.Cleanup(cleanup)
112118

113-
genesis := common.GetDefaultGenesis()
114-
genesis.AppState = gnoland.GnoGenesisState{
115-
Balances: balances,
116-
}
117-
require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
118-
119119
// Prepare the output file
120120
outputFile, outputCleanup := testutils.NewTestFile(t)
121121
t.Cleanup(outputCleanup)
122122

123-
// Create the command
124-
cmd := NewBalancesCmd(commands.NewTestIO())
125-
args := []string{
126-
"export",
127-
"--genesis-path",
128-
tempGenesis.Name(),
129-
outputFile.Name(),
123+
saveGenesisFile := func(outputPath string) {
124+
genesis := common.GetDefaultGenesis()
125+
genesis.AppState = gnoland.GnoGenesisState{
126+
Balances: balances,
127+
}
128+
require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
129+
130+
// Create the command
131+
cmd := NewBalancesCmd(commands.NewTestIO())
132+
args := []string{
133+
"export",
134+
"--genesis-path",
135+
tempGenesis.Name(),
136+
outputPath,
137+
}
138+
139+
// Run the command
140+
cmdErr := cmd.ParseAndRun(context.Background(), args)
141+
require.NoError(t, cmdErr)
130142
}
131143

132-
// Run the command
133-
cmdErr := cmd.ParseAndRun(context.Background(), args)
134-
require.NoError(t, cmdErr)
144+
saveGenesisFile(outputFile.Name())
145+
readIt := func(p string) string {
146+
blob, err := os.ReadFile(p)
147+
require.NoError(t, err)
148+
return string(blob)
149+
}
135150

136151
// Validate the transactions were written down
152+
outputFile.Seek(0, 0) // Seek back to the front of the outputFile.
137153
scanner := bufio.NewScanner(outputFile)
138154

139155
outputBalances := make([]gnoland.Balance, 0)
@@ -146,11 +162,46 @@ func TestGenesis_Balances_Export(t *testing.T) {
146162
}
147163

148164
require.NoError(t, scanner.Err())
149-
150165
assert.Len(t, outputBalances, len(balances))
151166

152-
for index, balance := range outputBalances {
153-
assert.Equal(t, balances[index], balance)
167+
// Next ensure that all balances are sorted by address, deterministically.
168+
for i := 1; i < len(outputBalances); i++ {
169+
curr := outputBalances[i].Address
170+
for j := 0; j < i; j++ {
171+
prev := outputBalances[j].Address
172+
if addressIsGreater(prev, curr) {
173+
t.Fatalf("Non-deterministic order of exported balances\n\t[%d](%s)\n>\n\t[%d](%s)", j, prev, i, curr)
174+
}
175+
}
176+
}
177+
178+
// Lastly compute the checksum and ensure that it is the same each of the N times.
179+
outputFile.Close()
180+
firstGenesisPathMD5 := md5SumFromFile(t, outputFile.Name())
181+
firstGenesis := readIt(outputFile.Name())
182+
for i := 1; i <= 10; i++ {
183+
outpath := filepath.Join(t.TempDir(), "out")
184+
saveGenesisFile(outpath)
185+
currentMD5 := md5SumFromFile(t, outpath)
186+
currentGenesis := readIt(outpath)
187+
require.Equal(t, firstGenesisPathMD5, currentMD5, "Iteration #%d has a different MD5 checksum\n%s\n\n%s", i, firstGenesis, currentGenesis)
154188
}
189+
155190
})
156191
}
192+
193+
func md5SumFromFile(t *testing.T, p string) string {
194+
f, err := os.Open(p)
195+
require.NoError(t, err)
196+
fi, err := f.Stat()
197+
require.NoError(t, err)
198+
require.True(t, fi.Size() > 100, "at least 100 bytes expected")
199+
defer f.Close()
200+
h := md5.New()
201+
io.Copy(h, f)
202+
return fmt.Sprintf("%x", h.Sum(nil))
203+
}
204+
205+
func addressIsGreater(a, b bft.Address) bool {
206+
return a.Compare(b) == 1
207+
}

Diff for: gno.land/pkg/gnoland/balance.go

+15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"fmt"
66
"io"
7+
"sort"
78
"strings"
89

910
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
@@ -79,14 +80,28 @@ func (bs Balances) Get(address crypto.Address) (balance Balance, ok bool) {
7980
return
8081
}
8182

83+
// List returns a slice of balances, sorted by Balance.Address
84+
// in lexicographic order.
8285
func (bs Balances) List() []Balance {
8386
list := make([]Balance, 0, len(bs))
8487
for _, balance := range bs {
8588
list = append(list, balance)
8689
}
90+
91+
SortBalances(list)
8792
return list
8893
}
8994

95+
// Sorts balances in lexicographic order, compared by .Address instead of .Amount
96+
// because .Amount's type is Coins that requires a deeper comparison by .Denom and
97+
// .Amount which are unnecessarily complex yet by the nature of each Balance in Balances,
98+
// each entry will be keyed by the same Address in a map.
99+
func SortBalances(list []Balance) {
100+
sort.Slice(list, func(i, j int) bool {
101+
return list[i].Address.Compare(list[j].Address) == -1
102+
})
103+
}
104+
90105
// LeftMerge left-merges the two maps
91106
func (bs Balances) LeftMerge(from Balances) {
92107
for key, bVal := range from {

Diff for: gno.land/pkg/gnoland/balance_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package gnoland
22

33
import (
4+
crand "crypto/rand"
45
"fmt"
56
"math"
7+
"math/rand"
68
"strconv"
79
"strings"
810
"testing"
@@ -274,3 +276,24 @@ func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey {
274276

275277
return secp256k1.PrivKeySecp256k1(derivedPriv)
276278
}
279+
280+
func TestBalancesList(t *testing.T) {
281+
// 1. Generate and insert balances.
282+
balances := NewBalances()
283+
n := 100
284+
rng := rand.New(rand.NewSource(10))
285+
for i := 0; i < n; i++ {
286+
var addr bft.Address
287+
crand.Read(addr[:])
288+
amount := std.NewCoins(std.NewCoin(ugnot.Denom, 1+rng.Int63n(100)))
289+
balances.Set(addr, amount)
290+
}
291+
292+
list := balances.List()
293+
for i := 1; i < len(list); i++ {
294+
for j := 0; j < i; j++ {
295+
isLess := compareAddresses(list[j].Address, list[i].Address) <= 0
296+
assert.True(t, isLess, "Address:\n\t#%d[%x]\n>\n\t#%d[%x]", j, list[j], i, list[i])
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)