diff --git a/fsm/genesis.go b/fsm/genesis.go index ac44e803cc..0b1ac801e0 100644 --- a/fsm/genesis.go +++ b/fsm/genesis.go @@ -231,6 +231,9 @@ type genesisState struct { Supply *Supply `json:"supply"` OrderBooks *lib.OrderBooks `protobuf:"bytes,7,opt,name=order_books,json=orderBooks,proto3" json:"orderBooks,omitempty"` DoubleSigners []*lib.DoubleSigner `protobuf:"bytes,6,rep,name=double_signers,json=doubleSigners,proto3" json:"doubleSigners,omitempty"` // only used for export + // RetiredCommittees must be serialized so that retired (non-subsidized) committees survive a + // genesis export/import round trip; otherwise they silently become live again on restart. + RetiredCommittees []uint64 `json:"retiredCommittees,omitempty"` } // MarshalJSON() is the json.Marshaller implementation for the GenesisState object @@ -240,15 +243,16 @@ func (x *GenesisState) MarshalJSON() ([]byte, error) { t = time.UnixMicro(int64(x.Time)).Format(time.DateTime) } return json.Marshal(genesisState{ - Time: t, - Pools: x.Pools, - Accounts: x.Accounts, - NonSigners: x.NonSigners, - Validators: x.Validators, - Params: x.Params, - Supply: x.Supply, - OrderBooks: x.OrderBooks, - DoubleSigners: x.DoubleSigners, + Time: t, + Pools: x.Pools, + Accounts: x.Accounts, + NonSigners: x.NonSigners, + Validators: x.Validators, + Params: x.Params, + Supply: x.Supply, + OrderBooks: x.OrderBooks, + DoubleSigners: x.DoubleSigners, + RetiredCommittees: x.RetiredCommittees, }) } @@ -268,5 +272,6 @@ func (x *GenesisState) UnmarshalJSON(bz []byte) (err error) { x.Params, x.Pools, x.Supply = ptr.Params, ptr.Pools, ptr.Supply x.Accounts, x.Validators, x.NonSigners = ptr.Accounts, ptr.Validators, ptr.NonSigners x.OrderBooks, x.DoubleSigners = ptr.OrderBooks, ptr.DoubleSigners + x.RetiredCommittees = ptr.RetiredCommittees return } diff --git a/fsm/genesis_test.go b/fsm/genesis_test.go index 06e200f62b..6c0c3ebbd2 100644 --- a/fsm/genesis_test.go +++ b/fsm/genesis_test.go @@ -816,3 +816,24 @@ func validateWithExportedState(t *testing.T, sm StateMachine, expected *GenesisS // compare got vs expected require.EqualExportedValues(t, expected, got, fmt.Sprintf("EXPECTED:\n%s\nGOT:\n%s", expectedJson, gotJson)) } + +// TestGenesisJSONRoundTripPreservesRetiredCommittees ensures that retired committees survive a +// GenesisState -> JSON -> GenesisState round trip. Retired committees are non-subsidized "for +// eternity"; if the JSON codec drops them, an export/import upgrade silently makes them live +// again and they resume receiving minted rewards. +func TestGenesisJSONRoundTripPreservesRetiredCommittees(t *testing.T) { + // a genesis state carrying two retired committees + expected := &GenesisState{ + Params: DefaultParams(), + RetiredCommittees: []uint64{7, 21}, + } + // marshal to JSON via the custom GenesisState codec + bz, err := json.Marshal(expected) + require.NoError(t, err) + // unmarshal back into a fresh GenesisState + got := new(GenesisState) + require.NoError(t, json.Unmarshal(bz, got)) + // the retired committees must survive the round trip + require.Equal(t, expected.RetiredCommittees, got.RetiredCommittees, + "retired committees must survive a genesis export/import round trip; json=%s", string(bz)) +}