diff --git a/api/api_full.go b/api/api_full.go index a04d5efc3..64d8080e7 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -896,6 +896,8 @@ type FullNode interface { IPCListCheckpoints(ctx context.Context, sn sdk.SubnetID, from, to abi.ChainEpoch) ([]*gateway.BottomUpCheckpoint, error) //perm:read IPCGetCheckpoint(ctx context.Context, sn sdk.SubnetID, epoch abi.ChainEpoch) (*gateway.BottomUpCheckpoint, error) //perm:read IPCGetTopDownMsgs(ctx context.Context, gatewayAddr address.Address, sn sdk.SubnetID, tsk types.TipSetKey, nonce uint64) ([]*gateway.CrossMsg, error) //perm:read + IPCGetGenesisEpochForSubnet(ctx context.Context, gatewayAddr address.Address, sn sdk.SubnetID) (abi.ChainEpoch, error) //perm:read + // Serialized representation of IPC calls. // This calls are serialized version of some of the IPC calls. They return directly the CBOR IPCGetCheckpointSerialized // version of the output of the call. These are really convenient to use the same type of serialization used diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 6eb29f400..930b43bda 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1674,6 +1674,21 @@ func (mr *MockFullNodeMockRecorder) IPCGetCheckpointTemplateSerialized(arg0, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IPCGetCheckpointTemplateSerialized", reflect.TypeOf((*MockFullNode)(nil).IPCGetCheckpointTemplateSerialized), arg0, arg1, arg2) } +// IPCGetGenesisEpochForSubnet mocks base method. +func (m *MockFullNode) IPCGetGenesisEpochForSubnet(arg0 context.Context, arg1 address.Address, arg2 sdk.SubnetID) (abi.ChainEpoch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IPCGetGenesisEpochForSubnet", arg0, arg1, arg2) + ret0, _ := ret[0].(abi.ChainEpoch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IPCGetGenesisEpochForSubnet indicates an expected call of IPCGetGenesisEpochForSubnet. +func (mr *MockFullNodeMockRecorder) IPCGetGenesisEpochForSubnet(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IPCGetGenesisEpochForSubnet", reflect.TypeOf((*MockFullNode)(nil).IPCGetGenesisEpochForSubnet), arg0, arg1, arg2) +} + // IPCGetPrevCheckpointForChild mocks base method. func (m *MockFullNode) IPCGetPrevCheckpointForChild(arg0 context.Context, arg1 address.Address, arg2 sdk.SubnetID) (cid.Cid, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 4b70f86f6..e8c4eab53 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -341,6 +341,8 @@ type FullNodeMethods struct { IPCGetCheckpointTemplateSerialized func(p0 context.Context, p1 address.Address, p2 abi.ChainEpoch) ([]byte, error) `perm:"read"` + IPCGetGenesisEpochForSubnet func(p0 context.Context, p1 address.Address, p2 sdk.SubnetID) (abi.ChainEpoch, error) `perm:"read"` + IPCGetPrevCheckpointForChild func(p0 context.Context, p1 address.Address, p2 sdk.SubnetID) (cid.Cid, error) `perm:"read"` IPCGetTopDownMsgs func(p0 context.Context, p1 address.Address, p2 sdk.SubnetID, p3 types.TipSetKey, p4 uint64) ([]*gateway.CrossMsg, error) `perm:"read"` @@ -2583,6 +2585,17 @@ func (s *FullNodeStub) IPCGetCheckpointTemplateSerialized(p0 context.Context, p1 return *new([]byte), ErrNotSupported } +func (s *FullNodeStruct) IPCGetGenesisEpochForSubnet(p0 context.Context, p1 address.Address, p2 sdk.SubnetID) (abi.ChainEpoch, error) { + if s.Internal.IPCGetGenesisEpochForSubnet == nil { + return *new(abi.ChainEpoch), ErrNotSupported + } + return s.Internal.IPCGetGenesisEpochForSubnet(p0, p1, p2) +} + +func (s *FullNodeStub) IPCGetGenesisEpochForSubnet(p0 context.Context, p1 address.Address, p2 sdk.SubnetID) (abi.ChainEpoch, error) { + return *new(abi.ChainEpoch), ErrNotSupported +} + func (s *FullNodeStruct) IPCGetPrevCheckpointForChild(p0 context.Context, p1 address.Address, p2 sdk.SubnetID) (cid.Cid, error) { if s.Internal.IPCGetPrevCheckpointForChild == nil { return *new(cid.Cid), ErrNotSupported diff --git a/build/actors/ipc-actors.car b/build/actors/ipc-actors.car index 486cbd717..de30df5f7 100644 Binary files a/build/actors/ipc-actors.car and b/build/actors/ipc-actors.car differ diff --git a/build/genesis/spacenet.car b/build/genesis/spacenet.car index 99b5829ee..b5b00bf6e 100644 Binary files a/build/genesis/spacenet.car and b/build/genesis/spacenet.car differ diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index fad06bf02..2dbeac68d 100644 Binary files a/build/openrpc/full.json.gz and b/build/openrpc/full.json.gz differ diff --git a/build/openrpc/gateway.json.gz b/build/openrpc/gateway.json.gz index df24046c3..ef3d15803 100644 Binary files a/build/openrpc/gateway.json.gz and b/build/openrpc/gateway.json.gz differ diff --git a/build/openrpc/miner.json.gz b/build/openrpc/miner.json.gz index 663f14f31..ddb5c0d60 100644 Binary files a/build/openrpc/miner.json.gz and b/build/openrpc/miner.json.gz differ diff --git a/build/openrpc/worker.json.gz b/build/openrpc/worker.json.gz index 9e5cc2e14..637799dc5 100644 Binary files a/build/openrpc/worker.json.gz and b/build/openrpc/worker.json.gz differ diff --git a/chain/consensus/mir/manager.go b/chain/consensus/mir/manager.go index 9c1712a64..6339e32cc 100644 --- a/chain/consensus/mir/manager.go +++ b/chain/consensus/mir/manager.go @@ -102,6 +102,7 @@ func NewManager(ctx context.Context, } initialValidatorSet := membershipInfo.ValidatorSet + genesisEpoch := membershipInfo.GenesisEpoch valSize := initialValidatorSet.Size() // There needs to be at least one validator in the membership if valSize == 0 { @@ -157,7 +158,7 @@ func NewManager(ctx context.Context, m.mirErrChan = make(chan error, 1) m.mirCtx, m.mirCancel = context.WithCancel(context.Background()) - m.stateManager, err = NewStateManager(ctx, initialMembership, m.confManager, node, ds, m.requestPool, cfg) + m.stateManager, err = NewStateManager(ctx, m.netName, initialMembership, abi.ChainEpoch(genesisEpoch), m.confManager, node, ds, m.requestPool, cfg) if err != nil { return nil, fmt.Errorf("validator %v failed to start mir state manager: %w", id, err) } diff --git a/chain/consensus/mir/membership/membership.go b/chain/consensus/mir/membership/membership.go index 1f8192d87..8a918edba 100644 --- a/chain/consensus/mir/membership/membership.go +++ b/chain/consensus/mir/membership/membership.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/consensus-shipyard/go-ipc-types/gateway" "github.com/consensus-shipyard/go-ipc-types/sdk" "github.com/consensus-shipyard/go-ipc-types/validator" "github.com/multiformats/go-multiaddr" @@ -41,6 +42,7 @@ func IsSourceValid(source string) error { type Info struct { MinValidators uint64 ValidatorSet *validator.Set + GenesisEpoch uint64 } type Reader interface { @@ -124,6 +126,7 @@ func NewOnChainMembershipClient(client rpc.JSONRPCRequestSender, subnet sdk.Subn type AgentResponse struct { ValidatorSet validator.Set `json:"validator_set"` MinValidators uint64 `json:"min_validators"` + GenesisEpoch uint64 `json:"genesis_epoch"` } // GetMembershipInfo gets the membership config from the actor state. @@ -142,6 +145,7 @@ func (c *OnChainMembership) GetMembershipInfo() (*Info, error) { return &Info{ ValidatorSet: &resp.ValidatorSet, MinValidators: resp.MinValidators, + GenesisEpoch: resp.GenesisEpoch, }, nil } @@ -182,13 +186,50 @@ func NewSetMembershipMsg(gw address.Address, valSet *validator.Set) (*types.Sign GasFeeCap: types.NewInt(0), GasPremium: types.NewInt(0), GasLimit: build.BlockGasLimit, // Make super sure this is never too little + Nonce: 0, } return &types.SignedMessage{Message: msg, Signature: crypto.Signature{Type: crypto.SigTypeDelegated}}, nil } -// IsConfigMsg determines if the message is a config message to set on-chain membership. +// NewInitGenesisEpochMsg creates a new config message to initialize +// implicitly the subnet and set the genesis epoch for it. +func NewInitGenesisEpochMsg(gw address.Address, genesisEpoch abi.ChainEpoch) (*types.SignedMessage, error) { + params, err := actors.SerializeParams(&gateway.InitGenesisEpochParams{GenesisEpoch: genesisEpoch}) + if err != nil { + return nil, err + } + msg := types.Message{ + To: gw, + From: builtin.SystemActorAddr, + Value: abi.NewTokenAmount(0), + Method: builtin.MustGenerateFRCMethodNum("InitGenesisEpoch"), + Params: params, + GasFeeCap: types.NewInt(0), + GasPremium: types.NewInt(0), + GasLimit: build.BlockGasLimit, // Make super sure this is never too little + // the nonce must be different than other config messages for the case where + // all config messages are included in the same block, if not the one with the + // largest nonce will be discarded. + Nonce: 1, + } + return &types.SignedMessage{Message: msg, Signature: crypto.Signature{Type: crypto.SigTypeDelegated}}, nil +} + +// IsConfigMsg determines if the message is an on-chain configuration message. func IsConfigMsg(gw address.Address, msg *types.Message) bool { + return IsSetMembershipConfigMsg(gw, msg) || IsInitGenesisEpochConfigMsg(gw, msg) +} + +// IsSetMembershipConfigMsg determines if the message sets membership. +func IsSetMembershipConfigMsg(gw address.Address, msg *types.Message) bool { return msg.To == gw && msg.From == builtin.SystemActorAddr && msg.Method == builtin.MustGenerateFRCMethodNum("SetMembership") } + +// IsInitGenesisEpochConfigMsg determines if the message initializes the genesis epoch. +func IsInitGenesisEpochConfigMsg(gw address.Address, msg *types.Message) bool { + return msg.To == gw && + msg.From == builtin.SystemActorAddr && + msg.Method == builtin.MustGenerateFRCMethodNum("InitGenesisEpoch") +} diff --git a/chain/consensus/mir/state_manager.go b/chain/consensus/mir/state_manager.go index d643767ef..0647dfc9e 100644 --- a/chain/consensus/mir/state_manager.go +++ b/chain/consensus/mir/state_manager.go @@ -7,6 +7,7 @@ import ( "path" "time" + "github.com/consensus-shipyard/go-ipc-types/sdk" "github.com/consensus-shipyard/go-ipc-types/validator" "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" @@ -28,6 +29,7 @@ import ( "github.com/filecoin-project/lotus/chain/gen/genesis" "github.com/filecoin-project/lotus/chain/types" ltypes "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/node/modules/dtypes" ) var ( @@ -52,7 +54,9 @@ type StateManager struct { ctx context.Context // Lotus API - api v1api.FullNode + api v1api.FullNode + netName dtypes.NetworkName + genesisEpoch abi.ChainEpoch // The current epoch number. currentEpoch t.EpochNr @@ -97,7 +101,9 @@ type StateManager struct { func NewStateManager( ctx context.Context, + netName dtypes.NetworkName, initialMembership map[t.NodeID]t.NodeAddress, + genesisEpoch abi.ChainEpoch, cm *ConfigurationManager, api v1api.FullNode, ds db.DB, @@ -106,6 +112,8 @@ func NewStateManager( ) (*StateManager, error) { sm := StateManager{ ctx: ctx, + netName: netName, + genesisEpoch: genesisEpoch, nextCheckpointChan: make(chan *checkpoint.StableCheckpoint, 1), confManager: cm, ds: ds, @@ -292,14 +300,22 @@ func (sm *StateManager) ApplyTXs(txs []*requestpb.Request) error { sm.height++ - // Include initial membership into the block 1. + // Include initial configuration and subnet initialization into the block 1. if sm.height == 1 { info := sm.confManager.GetInitialMembershipInfo() initialConfigMsg, err := membership.NewSetMembershipMsg(genesis.DefaultIPCGatewayAddr, info.ValidatorSet) if err != nil { - return err + return xerrors.Errorf("error setting initial on-chain membership: %w", err) } valSetMsgs = append(valSetMsgs, initialConfigMsg) + // the rootnet doesn't need to be explicitly initialized. + if string(sm.netName) != sdk.RootStr { + initializeMsg, err := membership.NewInitGenesisEpochMsg(genesis.DefaultIPCGatewayAddr, sm.genesisEpoch) + if err != nil { + return xerrors.Errorf("error initializing subnet: %w", err) + } + valSetMsgs = append(valSetMsgs, initializeMsg) + } } // For each request in the batch @@ -384,7 +400,6 @@ func (sm *StateManager) ApplyTXs(txs []*requestpb.Request) error { if err != nil { return xerrors.Errorf("validator %v unable to sync a block: %w", sm.id, err) } - log.With("validator", sm.id).With("epoch", sm.currentEpoch).Infof("mined block %d : %v ", bh.Header.Height, bh.Header.Cid()) return nil diff --git a/chain/gen/genesis/f08_ipc_gateway.go b/chain/gen/genesis/f08_ipc_gateway.go index 60e661ca8..4d4a2d959 100644 --- a/chain/gen/genesis/f08_ipc_gateway.go +++ b/chain/gen/genesis/f08_ipc_gateway.go @@ -4,6 +4,7 @@ import ( "context" "github.com/consensus-shipyard/go-ipc-types/gateway" + "github.com/consensus-shipyard/go-ipc-types/sdk" ipctypes "github.com/consensus-shipyard/go-ipc-types/sdk" "github.com/consensus-shipyard/go-ipc-types/voting" cbor "github.com/ipfs/go-ipld-cbor" @@ -46,6 +47,12 @@ func constructState(store adt.Store, network ipctypes.SubnetID, buPeriod, tdPeri return nil, xerrors.Errorf("failed to create empty map: %w", err) } + // if it is the rootnet no need to explicitly initialize the gateway. + initialized := false + if network == sdk.RootSubnet { + initialized = true + } + return &gateway.State{ NetworkName: network, TotalSubnets: 0, @@ -58,6 +65,7 @@ func constructState(store adt.Store, network ipctypes.SubnetID, buPeriod, tdPeri BottomupNonce: 0, AppliedTopdownNonce: 0, TopDownCheckpointVoting: voting, + Initialized: initialized, }, nil } diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 0b2e4b8c0..bf19aaa3b 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -119,6 +119,7 @@ * [IPCGetCheckpointSerialized](#IPCGetCheckpointSerialized) * [IPCGetCheckpointTemplate](#IPCGetCheckpointTemplate) * [IPCGetCheckpointTemplateSerialized](#IPCGetCheckpointTemplateSerialized) + * [IPCGetGenesisEpochForSubnet](#IPCGetGenesisEpochForSubnet) * [IPCGetPrevCheckpointForChild](#IPCGetPrevCheckpointForChild) * [IPCGetTopDownMsgs](#IPCGetTopDownMsgs) * [IPCGetTopDownMsgsSerialized](#IPCGetTopDownMsgsSerialized) @@ -3524,6 +3525,24 @@ Inputs: Response: `"Ynl0ZSBhcnJheQ=="` +### IPCGetGenesisEpochForSubnet + + +Perms: read + +Inputs: +```json +[ + "f01234", + { + "Parent": "string value", + "Actor": "f01234" + } +] +``` + +Response: `10101` + ### IPCGetPrevCheckpointForChild @@ -3852,7 +3871,8 @@ Response: }, "Sig": "Ynl0ZSBhcnJheQ==" }, - "AppliedBottomupNonce": 42 + "AppliedBottomupNonce": 42, + "GenesisEpoch": 10101 } ] ``` @@ -3926,7 +3946,8 @@ Response: "configuration_number": 42 }, "TotalWeight": "0" - } + }, + "Initialized": true } ``` @@ -3972,7 +3993,6 @@ Response: "Genesis": "Ynl0ZSBhcnJheQ==", "BottomUpCheckPeriod": 10101, "TopDownCheckPeriod": 10101, - "GenesisEpoch": 10101, "CommittedCheckpoints": { "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, diff --git a/go.mod b/go.mod index 5044612ef..b1cb4451c 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 github.com/buger/goterm v1.0.3 github.com/chzyer/readline v1.5.0 - github.com/consensus-shipyard/go-ipc-types v0.1.5-0.20230415084926-a980f00efdab + github.com/consensus-shipyard/go-ipc-types v0.1.5-0.20230417164942-65210e7fab95 github.com/containerd/cgroups v1.0.4 github.com/coreos/go-systemd/v22 v22.5.0 github.com/detailyang/go-fallocate v0.0.0-20180908115635-432fa640bd2e diff --git a/go.sum b/go.sum index 7b6b50ef9..5cdae59de 100644 --- a/go.sum +++ b/go.sum @@ -179,8 +179,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= -github.com/consensus-shipyard/go-ipc-types v0.1.5-0.20230415084926-a980f00efdab h1:e2w5asoK16Xx5+yM5YLZWn2+efC0xlcFoFSCnsuvjVA= -github.com/consensus-shipyard/go-ipc-types v0.1.5-0.20230415084926-a980f00efdab/go.mod h1:oZQ3wFTjxmAQLFZrHkj7pagpNF7Oq+GOy/bpjjgD83A= +github.com/consensus-shipyard/go-ipc-types v0.1.5-0.20230417164942-65210e7fab95 h1:/S9hKA0N+NDmLFN8sP+YpPSxUdyhewBZKWB0yX1o85I= +github.com/consensus-shipyard/go-ipc-types v0.1.5-0.20230417164942-65210e7fab95/go.mod h1:oZQ3wFTjxmAQLFZrHkj7pagpNF7Oq+GOy/bpjjgD83A= github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= diff --git a/itests/ipc_test.go b/itests/ipc_test.go index 60fd9bf3c..d6028a5c2 100644 --- a/itests/ipc_test.go +++ b/itests/ipc_test.go @@ -73,6 +73,11 @@ func TestIPCAccessors(t *testing.T) { JoinSubnet(t, ctx, api, src, actorAddr) + e, err := api.IPCGetGenesisEpochForSubnet(ctx, genesis.DefaultIPCGatewayAddr, sn) + require.NoError(t, err) + // require that the genesis epoch is greater than zero + require.True(t, e > 0) + // get subnet actor state _, err = api.IPCReadSubnetActorState(ctx, sn, types.EmptyTSK) require.NoError(t, err) diff --git a/itests/kit/mir_tools.go b/itests/kit/mir_tools.go index 363bda117..b787941a3 100644 --- a/itests/kit/mir_tools.go +++ b/itests/kit/mir_tools.go @@ -18,6 +18,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/chain/consensus" "github.com/filecoin-project/lotus/api/v1api" "github.com/filecoin-project/lotus/chain/consensus/mir" @@ -127,7 +128,18 @@ func MirNodesWaitForInitialConfigInFirstBlock(ctx context.Context, expected *val return err } + var membershipMsgNumber int + epochMsgSeen := false + for _, msg := range msgs.SecpkMessages { + + if membership.IsSetMembershipConfigMsg(consensus.DefaultGatewayAddr, &msg.Message) { + membershipMsgNumber++ + } + if membership.IsInitGenesisEpochConfigMsg(consensus.DefaultGatewayAddr, &msg.Message) { + epochMsgSeen = true + } + var valSet validator.Set if err := valSet.UnmarshalCBOR(bytes.NewReader(msg.Message.Params)); err != nil { return err @@ -137,6 +149,14 @@ func MirNodesWaitForInitialConfigInFirstBlock(ctx context.Context, expected *val return fmt.Errorf("expected %v, got %v", expected, valSet) } } + + if membershipMsgNumber != 1 { + return fmt.Errorf("%d SetMembershipConfigMsg were received", membershipMsgNumber) + } + // The tests are running on the rootnet only. + if epochMsgSeen { + return fmt.Errorf("epochMsgSeen message was received on the rootnet") + } } return nil diff --git a/node/impl/ipc/ipc.go b/node/impl/ipc/ipc.go index 7a13c0c30..4f82a191c 100644 --- a/node/impl/ipc/ipc.go +++ b/node/impl/ipc/ipc.go @@ -370,6 +370,23 @@ func (a *IPCAPI) IPCGetTopDownMsgsSerialized(ctx context.Context, gatewayAddr ad return out, nil } +// IPCGetGenesisEpochForSubnet returns the genesis epoch from which a subnet has been +// registered in the parent. +func (a *IPCAPI) IPCGetGenesisEpochForSubnet(ctx context.Context, gatewayAddr address.Address, sn sdk.SubnetID) (abi.ChainEpoch, error) { + st, err := a.IPCReadGatewayState(ctx, gatewayAddr, types.EmptyTSK) + if err != nil { + return 0, err + } + subnet, found, err := st.GetSubnet(a.Chain.ActorStore(ctx), sn) + if err != nil { + return 0, xerrors.Errorf("error getting subnet: %w", err) + } + if !found { + return 0, xerrors.Errorf("subnet not found in gateway") + } + return subnet.GenesisEpoch, nil +} + // readActorState reads the state of a specific actor at a specefic epoch determined by the tipset key. // // The function accepts the address actor and the tipSetKet from which to read the state as an input, along