diff --git a/contribs/gnogenesis/internal/fork/source_rpc_test.go b/contribs/gnogenesis/internal/fork/source_rpc_test.go index 8b9e34a1505..d0028f57f1c 100644 --- a/contribs/gnogenesis/internal/fork/source_rpc_test.go +++ b/contribs/gnogenesis/internal/fork/source_rpc_test.go @@ -3,6 +3,8 @@ package fork import ( "testing" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" @@ -162,3 +164,111 @@ func TestBruteForceSignerSequence(t *testing.T) { require.Error(t, err) }) } + +// makeGnoAccountWire builds the exact wire bytes that gno.land's auth handler +// returns for `auth/accounts/`: amino.MarshalJSONIndent of the concrete +// *gnoland.GnoAccount that acck.GetAccount yields on a gno.land chain. +// See tm2/pkg/sdk/auth/handler.go queryAccount. +func makeGnoAccountWire(t *testing.T, acc *gnoland.GnoAccount) []byte { + t.Helper() + bz, err := amino.MarshalJSONIndent(acc, "", " ") + require.NoError(t, err) + return bz +} + +// TestQueryAccountAtHeight_WireFormatContract guards the regression fixed in +// this PR: auth/accounts/ on gno.land returns a GnoAccount (BaseAccount +// + Attributes), and amino's strict-field policy rejects the previous decoders +// (wrapper-with-BaseAccount and bare std.BaseAccount). queryAccountAtHeight +// must decode as gnoland.GnoAccount. +func TestQueryAccountAtHeight_WireFormatContract(t *testing.T) { + t.Parallel() + + priv := ed25519.GenPrivKey() + pub := priv.PubKey() + src := &gnoland.GnoAccount{ + BaseAccount: std.BaseAccount{ + Address: pub.Address(), + Coins: std.NewCoins(std.NewCoin("ugnot", 12345)), + PubKey: pub, + AccountNumber: 42, + Sequence: 7, + }, + // Non-zero attributes — the field that broke the old decoders. + Attributes: gnoland.BitSet(0x3), + } + wire := makeGnoAccountWire(t, src) + + t.Run("decodes as GnoAccount and round-trips BaseAccount", func(t *testing.T) { + t.Parallel() + var got gnoland.GnoAccount + require.NoError(t, amino.UnmarshalJSON(wire, &got)) + + assert.Equal(t, src.Address, got.Address) + assert.Equal(t, src.AccountNumber, got.GetAccountNumber()) + assert.Equal(t, src.Sequence, got.GetSequence()) + assert.Equal(t, src.Attributes, got.Attributes) + assert.True(t, src.Coins.IsEqual(got.Coins), "coins mismatch: want %s, got %s", src.Coins, got.Coins) + }) + + t.Run("regression: old wrapper-with-BaseAccount decoder errors", func(t *testing.T) { + t.Parallel() + // First decoder the buggy code tried. amino's strict-field policy + // rejects the unknown "attributes" key. The buggy production path was + // gated on (err == nil), so the runtime-relevant property is the error + // itself — amino does partially populate the BaseAccount field before + // failing, but the buggy caller never returned that value. + var wrapper struct { + BaseAccount std.BaseAccount `json:"BaseAccount"` + } + err := amino.UnmarshalJSON(wire, &wrapper) + require.Error(t, err, "old wrapper decoder must fail on real GnoAccount wire format") + }) + + t.Run("regression: old bare-BaseAccount fallback errors", func(t *testing.T) { + t.Parallel() + // Fallback the buggy code tried second. Both "BaseAccount" and + // "attributes" are unknown to a bare BaseAccount, so amino rejects the + // payload. The buggy caller was gated on err == nil, so the property + // we lock in is the error itself; amino's partial-fill behavior on + // the target isn't part of the contract we depend on. + var bare std.BaseAccount + err := amino.UnmarshalJSON(wire, &bare) + require.Error(t, err, "old bare-BaseAccount decoder must fail on real GnoAccount wire format") + }) + + t.Run("decodes a zero-attributes GnoAccount", func(t *testing.T) { + t.Parallel() + // Common on-chain case: a freshly created account with no flags set. + zero := &gnoland.GnoAccount{ + BaseAccount: std.BaseAccount{ + Address: pub.Address(), + PubKey: pub, + AccountNumber: 1, + Sequence: 0, + }, + } + zwire := makeGnoAccountWire(t, zero) + + var got gnoland.GnoAccount + require.NoError(t, amino.UnmarshalJSON(zwire, &got)) + assert.Equal(t, uint64(1), got.GetAccountNumber()) + assert.Equal(t, uint64(0), got.GetSequence()) + assert.Equal(t, gnoland.BitSet(0), got.Attributes) + }) + + t.Run("zero-address GnoAccount decodes with IsZero address", func(t *testing.T) { + t.Parallel() + // On gno.land, querying an unknown address returns a zero-valued + // account rather than an error. queryAccountAtHeight detects this via + // acc.Address.IsZero() and returns nil. Lock in the wire-format + // assumption that backs that branch. + zero := &gnoland.GnoAccount{} + zwire := makeGnoAccountWire(t, zero) + + var got gnoland.GnoAccount + require.NoError(t, amino.UnmarshalJSON(zwire, &got)) + assert.True(t, got.Address.IsZero(), + "zero-valued GnoAccount must round-trip to an IsZero address") + }) +} diff --git a/contribs/gnogenesis/internal/fork/source_txs_rpc.go b/contribs/gnogenesis/internal/fork/source_txs_rpc.go index d33217fcbd1..10350784d97 100644 --- a/contribs/gnogenesis/internal/fork/source_txs_rpc.go +++ b/contribs/gnogenesis/internal/fork/source_txs_rpc.go @@ -148,28 +148,27 @@ func (s *rpcTxsSource) queryAccountAtHeight( return nil } if res.Response.Error != nil { + io.Printf("\n WARNING: account query returned error for %s at height %d: %v\n", + addr, height, res.Response.Error) return nil } if len(res.Response.Data) == 0 { + io.Printf("\n WARNING: empty account response for %s at height %d\n", addr, height) return nil } - // Response data is amino JSON (the auth query handler returns JSON). - // Try wrapped form first {"BaseAccount": {...}}, then direct. - var wrapper struct { - BaseAccount std.BaseAccount `json:"BaseAccount"` - } - if err := amino.UnmarshalJSON(res.Response.Data, &wrapper); err == nil { - return &wrapper.BaseAccount - } - - var acc std.BaseAccount + // Decode as gnoland.GnoAccount; the auth handler returns the concrete + // account type the gno.land app installs. + var acc gnoland.GnoAccount if err := amino.UnmarshalJSON(res.Response.Data, &acc); err != nil { io.Printf("\n WARNING: could not decode account %s at height %d: %v\n", addr, height, err) return nil } - return &acc + if acc.Address.IsZero() { + return nil + } + return &acc.BaseAccount } // newPooledFetcher wires this source's client pool into a pooledFetcher