diff --git a/x/bank/types/balance.go b/x/bank/types/balance.go index a84845df4a..a986398449 100644 --- a/x/bank/types/balance.go +++ b/x/bank/types/balance.go @@ -3,6 +3,7 @@ package types import ( "bytes" "encoding/json" + "fmt" "sort" "cosmossdk.io/x/bank/exported" @@ -51,7 +52,7 @@ func (b balanceByAddress) Swap(i, j int) { b.balances[i], b.balances[j] = b.balances[j], b.balances[i] } -// SanitizeGenesisBalances sorts addresses and coin sets. +// SanitizeGenesisBalances checks for duplicates and sorts addresses and coin sets. func SanitizeGenesisBalances(balances []Balance) []Balance { // Given that this function sorts balances, using the standard library's // Quicksort based algorithms, we have algorithmic complexities of: @@ -64,12 +65,18 @@ func SanitizeGenesisBalances(balances []Balance) []Balance { // 1. Retrieve the address equivalents for each Balance's address. addresses := make([]sdk.AccAddress, len(balances)) + // 2. Track any duplicate addresses to avoid false positives on invariant checks. + seen := make(map[string]struct{}) for i := range balances { addr, _ := sdk.AccAddressFromBech32(balances[i].Address) addresses[i] = addr + if _, exists := seen[string(addr)]; exists { + panic(fmt.Sprintf("genesis state has a duplicate account: %q aka %x", balances[i].Address, addr)) + } + seen[string(addr)] = struct{}{} } - // 2. Sort balances. + // 3. Sort balances. sort.Sort(balanceByAddress{addresses: addresses, balances: balances}) return balances diff --git a/x/bank/types/balance_test.go b/x/bank/types/balance_test.go index 323639341d..2e0d3e7c51 100644 --- a/x/bank/types/balance_test.go +++ b/x/bank/types/balance_test.go @@ -1,6 +1,7 @@ package types_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -171,6 +172,44 @@ func TestSanitizeBalances(t *testing.T) { } } +func TestSanitizeBalancesDuplicates(t *testing.T) { + // 1. Generate balances + tokens := sdk.TokensFromConsensusPower(81, sdk.DefaultPowerReduction) + coin := sdk.NewCoin("benchcoin", tokens) + coins := sdk.Coins{coin} + addrs, _ := makeRandomAddressesAndPublicKeys(13) + + var balances []bank.Balance + for _, addr := range addrs { + balances = append(balances, bank.Balance{ + Address: addr.String(), + Coins: coins, + }) + } + + // 2. Add duplicate + dupIdx := 3 + balances = append(balances, balances[dupIdx]) + addr, _ := sdk.AccAddressFromBech32(balances[dupIdx].Address) + expectedError := fmt.Sprintf("genesis state has a duplicate account: %q aka %x", balances[dupIdx].Address, addr) + + // 3. Add more balances + coin2 := sdk.NewCoin("coinbench", tokens) + coins2 := sdk.Coins{coin2, coin} + addrs2, _ := makeRandomAddressesAndPublicKeys(31) + for _, addr := range addrs2 { + balances = append(balances, bank.Balance{ + Address: addr.String(), + Coins: coins2, + }) + } + + // 4. Execute SanitizeGenesisBalances and expect an error + require.PanicsWithValue(t, expectedError, func() { + bank.SanitizeGenesisBalances(balances) + }, "SanitizeGenesisBalances should panic with duplicate accounts") +} + func makeRandomAddressesAndPublicKeys(n int) (accL []sdk.AccAddress, pkL []*ed25519.PubKey) { for i := 0; i < n; i++ { pk := ed25519.GenPrivKey().PubKey().(*ed25519.PubKey)