Skip to content

Commit 8a57500

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 8a57500

File tree

4 files changed

+53
-5
lines changed

4 files changed

+53
-5
lines changed

contribs/gnogenesis/internal/balances/balances_export.go

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

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

contribs/gnogenesis/internal/balances/balances_export_test.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func TestGenesis_Balances_Export(t *testing.T) {
105105
t.Parallel()
106106

107107
// Generate dummy balances
108-
balances := getDummyBalances(t, 10)
108+
balances := getDummyBalances(t, 100)
109109

110110
tempGenesis, cleanup := testutils.NewTestFile(t)
111111
t.Cleanup(cleanup)
@@ -146,11 +146,17 @@ func TestGenesis_Balances_Export(t *testing.T) {
146146
}
147147

148148
require.NoError(t, scanner.Err())
149-
150149
assert.Len(t, outputBalances, len(balances))
151150

152-
for index, balance := range outputBalances {
153-
assert.Equal(t, balances[index], balance)
151+
// Lastly ensure that all balances are sorted by address, deterministically.
152+
for i := 1; i < len(outputBalances); i++ {
153+
for j := 0; j < i; i++ {
154+
prev := outputBalances[j].Address
155+
curr := outputBalances[i].Address
156+
if balanceCompare(prev, curr) == 1 {
157+
t.Fatalf("Non-deterministic order of exported balances\n\t[%d](%x)\n>\n\t[%d](%x)", i-1, prev, i, curr)
158+
}
159+
}
154160
}
155161
})
156162
}

gno.land/pkg/gnoland/balance.go

+17
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package gnoland
22

33
import (
44
"bufio"
5+
"bytes"
56
"fmt"
67
"io"
8+
"sort"
79
"strings"
810

911
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
@@ -79,14 +81,29 @@ func (bs Balances) Get(address crypto.Address) (balance Balance, ok bool) {
7981
return
8082
}
8183

84+
// List returns a slice of balances, sorted by Balance.Address
85+
// in lexicographic order.
8286
func (bs Balances) List() []Balance {
8387
list := make([]Balance, 0, len(bs))
8488
for _, balance := range bs {
8589
list = append(list, balance)
8690
}
91+
92+
// Ensure that the balances are returned in a deterministic order.
93+
// We are comparing by Address instead of .Amount whose type is Coins
94+
// that then requires a deeper comparison by .Denom and .Amount which
95+
// is unnecessary yet by the nature of each amount, each address will
96+
// contain all the coins.
97+
sort.Slice(list, func(i, j int) bool {
98+
return compareAddresses(list[i].Address, list[j].Address) == -1
99+
})
87100
return list
88101
}
89102

103+
func compareAddresses(a, b bft.Address) int {
104+
return bytes.Compare([]byte(a[:]), []byte(b[:]))
105+
}
106+
90107
// LeftMerge left-merges the two maps
91108
func (bs Balances) LeftMerge(from Balances) {
92109
for key, bVal := range from {

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)