feat: implement ChainGetTipSet in Lotus v2 APIs (#13003)

Introduce the first API for Lotus v2, focusing on `ChainGetTipSet`
within the `Chain` group. Define `TipSetSelector` for advanced
tipset retrieval options, and create a compact JSON-RPC call format.

Gracefully accommodate both EC and F3 finalized tipsets based on node
configuration, where:
* EC finalized tipset is returned when F3 is turned off, has no
  finalized tipset or F3 isn't ready.
* F3 finalized is returned otherwise.

Support three categories of selectors under `TipSetSelector`:
* By tag: either "latest" or "finalized."
* By height: epoch, plus optional fallback to previous non-null tipset.
* By tipset key.

The selection falls back to tag "latest" if the user specifies no
selection criterion.

The JSON-RPC format is designed to use JSON Object as the parameters
passed to the RPC call to remain compact, and extensible.
This commit is contained in:
Masih H. Derkani
2025-04-09 17:08:59 +01:00
committed by GitHub
parent 9c897c58a0
commit a1f1d51b73
29 changed files with 1179 additions and 34 deletions

View File

@ -10,6 +10,7 @@
# UNRELEASED # UNRELEASED
- fix(eth): always return nil for eth transactions not found ([filecoin-project/lotus#12999](https://github.com/filecoin-project/lotus/pull/12999)) - fix(eth): always return nil for eth transactions not found ([filecoin-project/lotus#12999](https://github.com/filecoin-project/lotus/pull/12999))
- feat: add experimental v2 APIs that are "F3 aware." (TODO: expand this section significantly to cover where someone learns about the new APIs, how they enable them, and what expectations they should have around them—i.e., they may change)
# Node and Miner v1.32.2 / 2025-04-04 # Node and Miner v1.32.2 / 2025-04-04

View File

@ -38,6 +38,7 @@ import (
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
apitypes "github.com/filecoin-project/lotus/api/types" apitypes "github.com/filecoin-project/lotus/api/types"
"github.com/filecoin-project/lotus/api/v0api" "github.com/filecoin-project/lotus/api/v0api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/build/buildconstants" "github.com/filecoin-project/lotus/build/buildconstants"
"github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/actors/builtin/miner"
"github.com/filecoin-project/lotus/chain/actors/builtin/verifreg" "github.com/filecoin-project/lotus/chain/actors/builtin/verifreg"
@ -468,6 +469,7 @@ func init() {
}, },
Input: ecchain, Input: ecchain,
}) })
addExample(types.TipSetSelectors.Finalized)
} }
func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) {
@ -508,6 +510,15 @@ func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []r
default: default:
panic("unknown type") panic("unknown type")
} }
case "v2api":
switch name {
case "FullNode":
i = &v2api.FullNodeStruct{}
t = reflect.TypeOf(new(struct{ v2api.FullNode })).Elem()
permStruct = append(permStruct, reflect.TypeOf(v2api.FullNodeStruct{}.Internal))
default:
panic("unknown type")
}
} }
return return
} }

5
api/v2api/doc.go Normal file
View File

@ -0,0 +1,5 @@
// Package v2api provides an interface for interacting with the v2 full node APIs
// within the Filecoin network.
//
// This package is experimental and the API may change without notice.
package v2api

52
api/v2api/full.go Normal file
View File

@ -0,0 +1,52 @@
package v2api
import (
"context"
"github.com/filecoin-project/lotus/chain/types"
)
//go:generate go run github.com/golang/mock/mockgen -destination=v2mocks/mock_full.go -package=v2mocks . FullNode
// FullNode represents an interface for the v2 full node APIs. This interface
// currently consists of chain-related functionalities and the API is
// experimental and subject to change.
type FullNode interface {
// MethodGroup: Chain
// The Chain method group contains methods for interacting with
// the blockchain.
//
// <b>Note: This API is experimental and may change in the future.<b/>
//
// Please see Filecoin V2 API design documentation for more details:
// - https://www.notion.so/filecoindev/Lotus-F3-aware-APIs-1cfdc41950c180ae97fef580e79427d5
// - https://www.notion.so/filecoindev/Filecoin-V2-APIs-1d0dc41950c1808b914de5966d501658
// ChainGetTipSet retrieves a tipset that corresponds to the specified selector
// criteria. The criteria can be provided in the form of a tipset key, a
// blockchain height including an optional fallback to previous non-null tipset,
// or a designated tag such as "latest" or "finalized".
//
// The "Finalized" tag returns the tipset that is considered finalized based on
// the consensus protocol of the current node, either Filecoin EC Finality or
// Filecoin Fast Finality (F3). The finalized tipset selection gracefully falls
// back to EC finality in cases where F3 isn't ready or not running.
//
// In a case where no selector is provided, an error is returned. The selector
// must be explicitly specified.
//
// For more details, refer to the types.TipSetSelector and
// types.NewTipSetSelector.
//
// Example usage:
//
// selector := types.TipSetSelectors.Latest
// tipSet, err := node.ChainGetTipSet(context.Background(), selector)
// if err != nil {
// fmt.Println("Error retrieving tipset:", err)
// return
// }
// fmt.Printf("Latest TipSet: %v\n", tipSet)
//
ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error) //perm:read
}

13
api/v2api/permissioned.go Normal file
View File

@ -0,0 +1,13 @@
package v2api
import (
"github.com/filecoin-project/go-jsonrpc/auth"
"github.com/filecoin-project/lotus/api"
)
func PermissionedFullAPI(a FullNode) FullNode {
var out FullNodeStruct
auth.PermissionedProxy(api.AllPermissions, api.DefaultPerms, a, &out.Internal)
return &out
}

37
api/v2api/proxy_gen.go Normal file
View File

@ -0,0 +1,37 @@
// Code generated by github.com/filecoin-project/lotus/gen/api. DO NOT EDIT.
package v2api
import (
"context"
"golang.org/x/xerrors"
"github.com/filecoin-project/lotus/chain/types"
)
var ErrNotSupported = xerrors.New("method not supported")
type FullNodeStruct struct {
Internal FullNodeMethods
}
type FullNodeMethods struct {
ChainGetTipSet func(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) `perm:"read"`
}
type FullNodeStub struct {
}
func (s *FullNodeStruct) ChainGetTipSet(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) {
if s.Internal.ChainGetTipSet == nil {
return nil, ErrNotSupported
}
return s.Internal.ChainGetTipSet(p0, p1)
}
func (s *FullNodeStub) ChainGetTipSet(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) {
return nil, ErrNotSupported
}
var _ FullNode = new(FullNodeStruct)

View File

@ -0,0 +1,52 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/filecoin-project/lotus/api/v2api (interfaces: FullNode)
// Package v2mocks is a generated GoMock package.
package v2mocks
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
types "github.com/filecoin-project/lotus/chain/types"
)
// MockFullNode is a mock of FullNode interface.
type MockFullNode struct {
ctrl *gomock.Controller
recorder *MockFullNodeMockRecorder
}
// MockFullNodeMockRecorder is the mock recorder for MockFullNode.
type MockFullNodeMockRecorder struct {
mock *MockFullNode
}
// NewMockFullNode creates a new mock instance.
func NewMockFullNode(ctrl *gomock.Controller) *MockFullNode {
mock := &MockFullNode{ctrl: ctrl}
mock.recorder = &MockFullNodeMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFullNode) EXPECT() *MockFullNodeMockRecorder {
return m.recorder
}
// ChainGetTipSet mocks base method.
func (m *MockFullNode) ChainGetTipSet(arg0 context.Context, arg1 types.TipSetSelector) (*types.TipSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ChainGetTipSet", arg0, arg1)
ret0, _ := ret[0].(*types.TipSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ChainGetTipSet indicates an expected call of ChainGetTipSet.
func (mr *MockFullNodeMockRecorder) ChainGetTipSet(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainGetTipSet", reflect.TypeOf((*MockFullNode)(nil).ChainGetTipSet), arg0, arg1)
}

View File

@ -0,0 +1,95 @@
{
"openrpc": "1.2.6",
"info": {
"title": "Lotus RPC API",
"version": "1.32.3-dev"
},
"methods": [
{
"name": "Filecoin.ChainGetTipSet",
"description": "```go\nfunc (s *FullNodeStruct) ChainGetTipSet(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) {\n\tif s.Internal.ChainGetTipSet == nil {\n\t\treturn nil, ErrNotSupported\n\t}\n\treturn s.Internal.ChainGetTipSet(p0, p1)\n}\n```",
"summary": "ChainGetTipSet retrieves a tipset that corresponds to the specified selector\ncriteria. The criteria can be provided in the form of a tipset key, a\nblockchain height including an optional fallback to previous non-null tipset,\nor a designated tag such as \"latest\" or \"finalized\".\n\nThe \"Finalized\" tag returns the tipset that is considered finalized based on\nthe consensus protocol of the current node, either Filecoin EC Finality or\nFilecoin Fast Finality (F3). The finalized tipset selection gracefully falls\nback to EC finality in cases where F3 isn't ready or not running.\n\nIn a case where no selector is provided, an error is returned. The selector\nmust be explicitly specified.\n\nFor more details, refer to the types.TipSetSelector and\ntypes.NewTipSetSelector.\n\nExample usage:\n\n\tselector := types.TipSetSelectors.Latest\n\ttipSet, err := node.ChainGetTipSet(context.Background(), selector)\n\tif err != nil {\n\t\tfmt.Println(\"Error retrieving tipset:\", err)\n\t\treturn\n\t}\n\tfmt.Printf(\"Latest TipSet: %v\\n\", tipSet)\n",
"paramStructure": "by-position",
"params": [
{
"name": "p1",
"description": "types.TipSetSelector",
"summary": "",
"schema": {
"examples": [
{
"tag": "finalized"
}
],
"additionalProperties": false,
"properties": {
"height": {
"additionalProperties": false,
"properties": {
"anchor": {
"additionalProperties": false,
"properties": {
"key": {
"additionalProperties": false,
"type": "object"
},
"tag": {
"type": "string"
}
},
"type": "object"
},
"at": {
"title": "number",
"type": "number"
},
"previous": {
"type": "boolean"
}
},
"type": "object"
},
"key": {
"additionalProperties": false,
"type": "object"
},
"tag": {
"type": "string"
}
},
"type": [
"object"
]
},
"required": true,
"deprecated": false
}
],
"result": {
"name": "*types.TipSet",
"description": "*types.TipSet",
"summary": "",
"schema": {
"examples": [
{
"Cids": null,
"Blocks": null,
"Height": 0
}
],
"additionalProperties": false,
"type": [
"object"
]
},
"required": true,
"deprecated": false
},
"deprecated": false,
"externalDocs": {
"description": "Github remote link",
"url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L26"
}
}
]
}

View File

@ -29,6 +29,21 @@ import (
"github.com/filecoin-project/lotus/node/repo" "github.com/filecoin-project/lotus/node/repo"
) )
var _ F3Backend = (*F3)(nil)
type F3Backend interface {
GetOrRenewParticipationTicket(_ context.Context, minerID uint64, previous api.F3ParticipationTicket, instances uint64) (api.F3ParticipationTicket, error)
Participate(_ context.Context, ticket api.F3ParticipationTicket) (api.F3ParticipationLease, error)
ListParticipants() []api.F3Participant
GetManifest(ctx context.Context) (*manifest.Manifest, error)
GetCert(ctx context.Context, instance uint64) (*certs.FinalityCertificate, error)
GetLatestCert(ctx context.Context) (*certs.FinalityCertificate, error)
GetPowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error)
GetF3PowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error)
IsRunning() bool
Progress() gpbft.InstanceProgress
}
type F3 struct { type F3 struct {
inner *f3.F3 inner *f3.F3
ec *ecWrapper ec *ecWrapper

View File

@ -0,0 +1,171 @@
package types
import (
"golang.org/x/xerrors"
"github.com/filecoin-project/go-state-types/abi"
)
var (
// TipSetTags represents the predefined set of tags for tipsets. The supported
// tags are:
// - Latest: the most recent tipset in the chain with the heaviest weight.
// - Finalized: the most recent tipset considered final by the node.
//
// See TipSetTag.
TipSetTags = struct {
Latest TipSetTag
Finalized TipSetTag
}{
Latest: TipSetTag("latest"),
Finalized: TipSetTag("finalized"),
}
// TipSetSelectors represents the predefined set of selectors for tipsets.
//
// See TipSetSelector.
TipSetSelectors = struct {
Latest TipSetSelector
Finalized TipSetSelector
Height func(abi.ChainEpoch, bool, *TipSetAnchor) TipSetSelector
Key func(TipSetKey) TipSetSelector
}{
Latest: TipSetSelector{Tag: &TipSetTags.Latest},
Finalized: TipSetSelector{Tag: &TipSetTags.Finalized},
Height: func(height abi.ChainEpoch, previous bool, anchor *TipSetAnchor) TipSetSelector {
return TipSetSelector{Height: &TipSetHeight{At: &height, Previous: previous, Anchor: anchor}}
},
Key: func(key TipSetKey) TipSetSelector { return TipSetSelector{Key: &key} },
}
// TipSetAnchors represents the predefined set of anchors for tipsets.
//
// See TipSetAnchor.
TipSetAnchors = struct {
Latest *TipSetAnchor
Finalized *TipSetAnchor
Key func(TipSetKey) *TipSetAnchor
}{
Latest: &TipSetAnchor{Tag: &TipSetTags.Latest},
Finalized: &TipSetAnchor{Tag: &TipSetTags.Finalized},
Key: func(key TipSetKey) *TipSetAnchor { return &TipSetAnchor{Key: &key} },
}
)
// TipSetTag is a string that represents a pointer to a tipset.
// See TipSetSelector.
type TipSetTag string
// TipSetSelector captures the selection criteria for a tipset.
//
// The supported criterion for selection is one of the following:
// - Key: the tipset key, see TipSetKey.
// - Height: the tipset height with an optional fallback to non-null parent, see TipSetHeight.
// - Tag: the tipset tag, either "latest" or "finalized", see TipSetTags.
//
// At most, one such criterion can be specified at a time. Otherwise, the
// criterion is considered to be invalid. See Validate.
//
// Experimental: This API is experimental and may change without notice.
type TipSetSelector struct {
Key *TipSetKey `json:"key,omitempty"`
Height *TipSetHeight `json:"height,omitempty"`
Tag *TipSetTag `json:"tag,omitempty"`
}
// Validate ensures that the TipSetSelector is valid. It checks that only one of
// the selection criteria is specified. If no criteria are specified, it returns
// nil, indicating that the default selection criteria should be used as defined
// by the Lotus API Specification.
func (tss TipSetSelector) Validate() error {
var criteria int
if tss.Key != nil {
criteria++
}
if tss.Tag != nil {
criteria++
}
if tss.Height != nil {
criteria++
if err := tss.Height.Validate(); err != nil {
return xerrors.Errorf("validating tipset height: %w", err)
}
}
if criteria != 1 {
return xerrors.Errorf("exactly one tipset selection criteria must be specified, found: %v", criteria)
}
return nil
}
// TipSetHeight is a criterion that selects a tipset At given height anchored to
// a given parent tipset.
//
// In a case where the tipset at given height is null, and Previous is true,
// it'll select the previous non-null tipset instead. Otherwise, it returns the
// null tipset at the given height.
//
// The Anchor may optionally be specified as TipSetTag, or TipSetKey. If
// specified, the selected tipset is guaranteed to be a child of the tipset
// specified by the anchor at the given height. Otherwise, the "finalized" TipSetTag
// is used as the Anchor.
//
// Experimental: This API is experimental and may change without notice.
type TipSetHeight struct {
At *abi.ChainEpoch `json:"at,omitempty"`
Previous bool `json:"previous,omitempty"`
Anchor *TipSetAnchor `json:"anchor,omitempty"`
}
// Validate ensures that the TipSetHeight is valid. It checks that the height is
// not negative and the Anchor is valid.
//
// A nil or a zero-valued height is considered to be valid.
func (tsh TipSetHeight) Validate() error {
if tsh.At == nil {
return xerrors.New("invalid tipset height: at epoch must be specified")
}
if *tsh.At < 0 {
return xerrors.New("invalid tipset height: epoch cannot be less than zero")
}
return tsh.Anchor.Validate()
}
// TipSetAnchor represents a tipset in the chain that can be used as an anchor
// for selecting a tipset. The anchor may be specified as a TipSetTag or a
// TipSetKey but not both. Defaults to TipSetTag "finalized" if neither are
// specified.
//
// See TipSetHeight.
//
// Experimental: This API is experimental and may change without notice.
type TipSetAnchor struct {
// TODO: We might want to rename the term "anchor" to "parent" if they're
// conceptually interchangeable. Because, it is easier to reuse a term that
// already exist compared to teaching people a new one. For now we'll keep it as
// "anchor" to keep consistent with the internal API design discussions. We will
// revisit the terminology here as the new API groups are added, namely
// StateSearchMsg.
Key *TipSetKey `json:"key,omitempty"`
Tag *TipSetTag `json:"tag,omitempty"`
}
// Validate ensures that the TipSetAnchor is valid. It checks that at most one
// of TipSetKey or TipSetTag is specified. Otherwise, it returns an error.
//
// Note that a nil or a zero-valued anchor is valid, and is considered to be
// equivalent to the default anchor, which is the tipset tagged as "finalized".
func (tsa *TipSetAnchor) Validate() error {
if tsa == nil {
// An unspecified Anchor is valid, because it's an optional field, and falls back
// to whatever the API decides the default to be.
return nil
}
if tsa.Key != nil && tsa.Tag != nil {
return xerrors.New("invalid tipset anchor: at most one of key or tag must be specified")
}
// Zero-valued anchor is valid, and considered to be an equivalent to whatever
// the API decides the default to be.
return nil
}

View File

@ -0,0 +1,81 @@
package types_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/filecoin-project/lotus/chain/types"
)
func TestTipSetSelector_Marshalling(t *testing.T) {
for _, test := range []struct {
name string
subject types.TipSetSelector
wantJson string
wantErr string
}{
{
name: "zero-valued",
wantErr: "exactly one tipset selection criteria must be specified, found: 0",
},
{
name: "height",
subject: types.TipSetSelectors.Height(123, true, nil),
wantJson: `{"height":{"at":123,"previous":true}}`,
},
{
name: "height anchored to finalized",
subject: types.TipSetSelectors.Height(123, true, types.TipSetAnchors.Finalized),
wantJson: `{"height":{"at":123,"previous":true,"anchor":{"tag":"finalized"}}}`,
},
{
name: "invalid height anchor",
subject: types.TipSetSelectors.Height(123, true, &types.TipSetAnchor{
Tag: &types.TipSetTags.Finalized,
Key: &types.TipSetKey{},
}),
wantErr: "at most one of key or tag",
},
{
name: "height with no epoch",
subject: types.TipSetSelector{
Height: &types.TipSetHeight{},
},
wantErr: "epoch must be specified",
},
{
name: "invalid height epoch",
subject: types.TipSetSelectors.Height(-1, false, nil),
wantErr: "epoch cannot be less than zero",
},
{
name: "key",
subject: types.TipSetSelectors.Key(types.TipSetKey{}),
wantJson: `{"key":[]}`,
},
{
name: "tag finalized",
subject: types.TipSetSelectors.Finalized,
wantJson: `{"tag":"finalized"}`,
},
{
name: "tag latest",
subject: types.TipSetSelectors.Latest,
wantJson: `{"tag":"latest"}`,
},
} {
t.Run(test.name, func(t *testing.T) {
err := test.subject.Validate()
if test.wantErr != "" {
require.ErrorContains(t, err, test.wantErr)
} else {
require.NoError(t, err)
gotMarshalled, err := json.Marshal(test.subject)
require.NoError(t, err)
require.JSONEq(t, test.wantJson, string(gotMarshalled))
}
})
}
}

View File

@ -31,6 +31,8 @@ import (
"github.com/filecoin-project/go-paramfetch" "github.com/filecoin-project/go-paramfetch"
lapi "github.com/filecoin-project/lotus/api" lapi "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v1api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/build/buildconstants" "github.com/filecoin-project/lotus/build/buildconstants"
"github.com/filecoin-project/lotus/chain/beacon/drand" "github.com/filecoin-project/lotus/chain/beacon/drand"
@ -392,9 +394,11 @@ var DaemonCmd = &cli.Command{
log.Warnf("unable to inject prometheus ipfs/go-metrics exporter; some metrics will be unavailable; err: %s", err) log.Warnf("unable to inject prometheus ipfs/go-metrics exporter; some metrics will be unavailable; err: %s", err)
} }
var api lapi.FullNode var v1 v1api.FullNode
var v2 v2api.FullNode
stop, err := node.New(ctx, stop, err := node.New(ctx,
node.FullAPI(&api, node.Lite(isLite)), node.FullAPI(&v1, node.Lite(isLite)),
node.FullAPIv2(&v2),
node.Base(), node.Base(),
node.Repo(r), node.Repo(r),
@ -424,7 +428,7 @@ var DaemonCmd = &cli.Command{
} }
if cctx.String("import-key") != "" { if cctx.String("import-key") != "" {
if err := importKey(ctx, api, cctx.String("import-key")); err != nil { if err := importKey(ctx, v1, cctx.String("import-key")); err != nil {
log.Errorf("importing key failed: %+v", err) log.Errorf("importing key failed: %+v", err)
} }
} }
@ -445,7 +449,7 @@ var DaemonCmd = &cli.Command{
} }
// Instantiate the full node handler. // Instantiate the full node handler.
h, err := node.FullNodeHandler(api, true, serverOptions...) h, err := node.FullNodeHandler(v1, v2, true, serverOptions...)
if err != nil { if err != nil {
return fmt.Errorf("failed to instantiate rpc handler: %s", err) return fmt.Errorf("failed to instantiate rpc handler: %s", err)
} }

View File

@ -0,0 +1,62 @@
# Groups
* [Chain](#Chain)
* [ChainGetTipSet](#ChainGetTipSet)
## Chain
The Chain method group contains methods for interacting with
the blockchain.
<b>Note: This API is experimental and may change in the future.<b/>
Please see Filecoin V2 API design documentation for more details:
- https://www.notion.so/filecoindev/Lotus-F3-aware-APIs-1cfdc41950c180ae97fef580e79427d5
- https://www.notion.so/filecoindev/Filecoin-V2-APIs-1d0dc41950c1808b914de5966d501658
### ChainGetTipSet
ChainGetTipSet retrieves a tipset that corresponds to the specified selector
criteria. The criteria can be provided in the form of a tipset key, a
blockchain height including an optional fallback to previous non-null tipset,
or a designated tag such as "latest" or "finalized".
The "Finalized" tag returns the tipset that is considered finalized based on
the consensus protocol of the current node, either Filecoin EC Finality or
Filecoin Fast Finality (F3). The finalized tipset selection gracefully falls
back to EC finality in cases where F3 isn't ready or not running.
In a case where no selector is provided, an error is returned. The selector
must be explicitly specified.
For more details, refer to the types.TipSetSelector and
types.NewTipSetSelector.
Example usage:
selector := types.TipSetSelectors.Latest
tipSet, err := node.ChainGetTipSet(context.Background(), selector)
if err != nil {
fmt.Println("Error retrieving tipset:", err)
return
}
fmt.Printf("Latest TipSet: %v\n", tipSet)
Perms: read
Inputs:
```json
[
{
"tag": "finalized"
}
]
```
Response:
```json
{
"Cids": null,
"Blocks": null,
"Height": 0
}
```

View File

@ -28,6 +28,7 @@ type Visitor struct {
func main() { func main() {
var lets errgroup.Group var lets errgroup.Group
lets.Go(generateApi) lets.Go(generateApi)
lets.Go(generateApiV2)
lets.Go(generateApiV0) lets.Go(generateApiV0)
if err := lets.Wait(); err != nil { if err := lets.Wait(); err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
@ -44,6 +45,10 @@ func generateApi() error {
return generate("./api", "api", "api", "./api/proxy_gen.go") return generate("./api", "api", "api", "./api/proxy_gen.go")
} }
func generateApiV2() error {
return generate("./api/v2api", "v2api", "v2api", "./api/v2api/proxy_gen.go")
}
func (v *Visitor) Visit(node ast.Node) ast.Visitor { func (v *Visitor) Visit(node ast.Node) ast.Visitor {
st, ok := node.(*ast.TypeSpec) st, ok := node.(*ast.TypeSpec)
if !ok { if !ok {

View File

@ -15,6 +15,7 @@ func main() {
lets.SetLimit(1) // TODO: Investigate why this can't run in parallel. lets.SetLimit(1) // TODO: Investigate why this can't run in parallel.
lets.Go(generateApiFull) lets.Go(generateApiFull)
lets.Go(generateApiV0Methods) lets.Go(generateApiV0Methods)
lets.Go(generateApiV2Methods)
lets.Go(generateStorage) lets.Go(generateStorage)
lets.Go(generateWorker) lets.Go(generateWorker)
lets.Go(generateOpenRpcGateway) lets.Go(generateOpenRpcGateway)
@ -53,6 +54,16 @@ func generateApiV0Methods() error {
} }
} }
func generateApiV2Methods() error {
if ainfo, err := docgen.ParseApiASTInfo("api/v2api/full.go", "FullNode", "v2api", "./api/v2api"); err != nil {
return err
} else if err := generateMarkdown("documentation/en/api-v2-unstable-methods.md", "FullNode", "v2api", ainfo); err != nil {
return err
} else {
return generateOpenRpc("build/openrpc/v2/full.json", "FullNode", "v2api", ainfo)
}
}
func generateApiFull() error { func generateApiFull() error {
if ainfo, err := docgen.ParseApiASTInfo("api/api_full.go", "FullNode", "api", "./api"); err != nil { if ainfo, err := docgen.ParseApiASTInfo("api/api_full.go", "FullNode", "api", "./api"); err != nil {
return err return err

344
itests/api_v2_test.go Normal file
View File

@ -0,0 +1,344 @@
package itests
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/filecoin-project/go-f3"
"github.com/filecoin-project/go-f3/certs"
"github.com/filecoin-project/go-f3/gpbft"
"github.com/filecoin-project/go-f3/manifest"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/actors/policy"
"github.com/filecoin-project/lotus/chain/lf3"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/itests/kit"
)
func TestAPIV2_GetTipSetThroughRPC(t *testing.T) {
const (
timeout = 2 * time.Minute
blockTime = 10 * time.Millisecond
f3FinalizedEpoch = 123
targetHeight = 20 + policy.ChainFinality
)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
t.Cleanup(cancel)
kit.QuietMiningLogs()
mockF3 := newMockF3Backend()
subject, _, network := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.F3Backend(mockF3))
network.BeginMining(blockTime)
subject.WaitTillChain(ctx, kit.HeightAtLeast(targetHeight))
var (
heaviest = func(t *testing.T) *types.TipSet {
head, err := subject.ChainHead(ctx)
require.NoError(t, err)
return head
}
ecFinalized = func(t *testing.T) *types.TipSet {
head, err := subject.ChainHead(ctx)
require.NoError(t, err)
ecFinalized, err := subject.ChainGetTipSetByHeight(ctx, head.Height()-policy.ChainFinality, head.Key())
require.NoError(t, err)
return ecFinalized
}
tipSetAtHeight = func(height abi.ChainEpoch) func(t *testing.T) *types.TipSet {
return func(t *testing.T) *types.TipSet {
ts, err := subject.ChainGetTipSetByHeight(ctx, height, types.EmptyTSK)
require.NoError(t, err)
return ts
}
}
internalF3Error = errors.New("lost hearing in left eye")
plausibleCert = func(t *testing.T) *certs.FinalityCertificate {
f3FinalisedTipSet := tipSetAtHeight(f3FinalizedEpoch)(t)
return &certs.FinalityCertificate{
ECChain: &gpbft.ECChain{
TipSets: []*gpbft.TipSet{{
Epoch: int64(f3FinalisedTipSet.Height()),
Key: f3FinalisedTipSet.Key().Bytes(),
}},
},
}
}
implausibleCert = &certs.FinalityCertificate{
ECChain: &gpbft.ECChain{
TipSets: []*gpbft.TipSet{{
Epoch: int64(1413),
Key: []byte(`🐠`),
}},
},
}
)
// The tests here use the raw JSON request form for testing to both test the API
// through RPC, and showcase what the raw request on the wire would look like at
// Layer 7 of ISO model.
for _, test := range []struct {
name string
when func(t *testing.T)
request string
wantTipSet func(t *testing.T) *types.TipSet
wantErr string
wantResponseStatus int
}{
{
name: "no selector is error",
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet"],"id":1}`,
wantErr: "Parse error",
wantResponseStatus: http.StatusInternalServerError,
},
{
name: "latest tag is ok",
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"latest"}],"id":1}`,
wantTipSet: heaviest,
wantResponseStatus: http.StatusOK,
},
{
name: "finalized tag when f3 disabled falls back to ec",
when: func(t *testing.T) {
mockF3.running = false
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`,
wantTipSet: ecFinalized,
wantResponseStatus: http.StatusOK,
},
{
name: "finalized tag is ok",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCertErr = nil
mockF3.latestCert = plausibleCert(t)
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`,
wantTipSet: tipSetAtHeight(f3FinalizedEpoch),
wantResponseStatus: http.StatusOK,
},
{
name: "finalized tag when f3 not ready falls back to ec",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = nil
mockF3.latestCertErr = api.ErrF3NotReady
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`,
wantTipSet: ecFinalized,
wantResponseStatus: http.StatusOK,
},
{
name: "finalized tag when f3 fails is error",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = nil
mockF3.latestCertErr = internalF3Error
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`,
wantErr: internalF3Error.Error(),
wantResponseStatus: http.StatusOK,
},
{
name: "latest tag when f3 fails is ok",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = nil
mockF3.latestCertErr = internalF3Error
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"latest"}],"id":1}`,
wantTipSet: heaviest,
wantResponseStatus: http.StatusOK,
},
{
name: "finalized tag when f3 is broken",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = implausibleCert
mockF3.latestCertErr = nil
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`,
wantErr: "decoding latest f3 cert tipset key",
wantResponseStatus: http.StatusOK,
},
{
name: "height with no anchor without f3 falling back to ec is ok",
when: func(t *testing.T) {
mockF3.running = false
mockF3.latestCert = nil
mockF3.latestCertErr = nil
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":20}}],"id":1}`,
wantTipSet: tipSetAtHeight(20),
wantResponseStatus: http.StatusOK,
},
{
name: "height with no epoch",
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{}}],"id":1}`,
wantErr: "epoch must be specified",
wantResponseStatus: http.StatusOK,
},
{
name: "height with no anchor before finalized epoch is ok",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = plausibleCert(t)
mockF3.latestCertErr = nil
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":111}}],"id":1}`,
wantTipSet: tipSetAtHeight(111),
wantResponseStatus: http.StatusOK,
},
{
name: "height with no anchor after finalized epoch is error",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = plausibleCert(t)
mockF3.latestCertErr = nil
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":145}}],"id":1}`,
wantErr: "looking for tipset with height greater than start point",
wantResponseStatus: http.StatusOK,
},
{
name: "height with no anchor when f3 fails is error",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = nil
mockF3.latestCertErr = internalF3Error
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":456}}],"id":1}`,
wantErr: internalF3Error.Error(),
wantResponseStatus: http.StatusOK,
},
{
name: "height with no anchor and nil f3 cert falling back to ec fails",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = nil
mockF3.latestCertErr = nil
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":111}}],"id":1}`,
wantErr: "looking for tipset with height greater than start point",
wantResponseStatus: http.StatusOK,
},
{
name: "height with anchor to latest",
when: func(t *testing.T) {
mockF3.running = true
mockF3.latestCert = plausibleCert(t)
mockF3.latestCertErr = nil
},
request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":890,"anchor":{"tag":"latest"}}}],"id":1}`,
wantTipSet: tipSetAtHeight(890),
wantResponseStatus: http.StatusOK,
},
} {
t.Run(test.name, func(t *testing.T) {
if test.when != nil {
test.when(t)
}
gotResponseCode, gotResponseBody := subject.DoRawRPCRequest(t, 2, test.request)
require.Equal(t, test.wantResponseStatus, gotResponseCode, string(gotResponseBody))
var resultOrError struct {
Result *types.TipSet `json:"result,omitempty"`
Error *struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
} `json:"error,omitempty"`
}
require.NoError(t, json.Unmarshal(gotResponseBody, &resultOrError))
if test.wantErr != "" {
require.Nil(t, resultOrError.Result)
require.Contains(t, resultOrError.Error.Message, test.wantErr)
} else {
require.Nil(t, resultOrError.Error)
require.Equal(t, test.wantTipSet(t), resultOrError.Result)
}
})
}
}
var _ lf3.F3Backend = (*mockF3Backend)(nil)
type mockF3Backend struct {
progress gpbft.InstanceProgress
latestCert *certs.FinalityCertificate
latestCertErr error
certs map[uint64]*certs.FinalityCertificate
participants map[uint64]struct{}
manifest *manifest.Manifest
running bool
}
func newMockF3Backend() *mockF3Backend {
return &mockF3Backend{
certs: make(map[uint64]*certs.FinalityCertificate),
participants: make(map[uint64]struct{}),
}
}
func (t *mockF3Backend) GetOrRenewParticipationTicket(_ context.Context, minerID uint64, _ api.F3ParticipationTicket, _ uint64) (api.F3ParticipationTicket, error) {
if !t.running {
return nil, f3.ErrF3NotRunning
}
return binary.BigEndian.AppendUint64(nil, minerID), nil
}
func (t *mockF3Backend) Participate(_ context.Context, ticket api.F3ParticipationTicket) (api.F3ParticipationLease, error) {
if !t.running {
return api.F3ParticipationLease{}, f3.ErrF3NotRunning
}
mid := binary.BigEndian.Uint64(ticket)
if _, ok := t.participants[mid]; !ok {
return api.F3ParticipationLease{}, api.ErrF3ParticipationTicketInvalid
}
return api.F3ParticipationLease{
Network: "fish",
Issuer: "fishmonger",
MinerID: mid,
FromInstance: t.progress.ID,
ValidityTerm: 5,
}, nil
}
func (t *mockF3Backend) GetManifest(context.Context) (*manifest.Manifest, error) {
if !t.running {
return nil, f3.ErrF3NotRunning
}
return t.manifest, nil
}
func (t *mockF3Backend) GetCert(_ context.Context, instance uint64) (*certs.FinalityCertificate, error) {
if !t.running {
return nil, f3.ErrF3NotRunning
}
return t.certs[instance], nil
}
func (t *mockF3Backend) GetLatestCert(context.Context) (*certs.FinalityCertificate, error) {
if !t.running {
return nil, f3.ErrF3NotRunning
}
return t.latestCert, t.latestCertErr
}
func (t *mockF3Backend) GetPowerTable(context.Context, types.TipSetKey) (gpbft.PowerEntries, error) {
return nil, nil
}
func (t *mockF3Backend) GetF3PowerTable(context.Context, types.TipSetKey) (gpbft.PowerEntries, error) {
return nil, nil
}
func (t *mockF3Backend) ListParticipants() []api.F3Participant { return nil }
func (t *mockF3Backend) IsRunning() bool { return t.running }
func (t *mockF3Backend) Progress() gpbft.InstanceProgress { return t.progress }

View File

@ -130,7 +130,7 @@ func TestF3_InactiveModes(t *testing.T) {
opts := []any{kit.MockProofs()} opts := []any{kit.MockProofs()}
if tc.mode == "disabled" { if tc.mode == "disabled" {
opts = append(opts, kit.F3Enabled(nil)) opts = append(opts, kit.F3Disabled())
} }
client, miner, ens := kit.EnsembleMinimal(t, opts...) client, miner, ens := kit.EnsembleMinimal(t, opts...)
@ -505,7 +505,7 @@ func setupWithStaticManifest(t *testing.T, manif *manifest.Manifest, testBootstr
AllowDynamicFinalize: !testBootstrap, AllowDynamicFinalize: !testBootstrap,
} }
nodeOpts := []kit.NodeOpt{kit.WithAllSubsystems(), kit.F3Enabled(cfg)} nodeOpts := []kit.NodeOpt{kit.WithAllSubsystems(), kit.F3Config(cfg)}
nodeOpts = append(nodeOpts, extraOpts...) nodeOpts = append(nodeOpts, extraOpts...)
minerOpts := []kit.NodeOpt{kit.WithAllSubsystems(), kit.ConstructorOpts(node.Override(node.F3Participation, modules.F3Participation))} minerOpts := []kit.NodeOpt{kit.WithAllSubsystems(), kit.ConstructorOpts(node.Override(node.F3Participation, modules.F3Participation))}
minerOpts = append(minerOpts, extraOpts...) minerOpts = append(minerOpts, extraOpts...)

View File

@ -587,7 +587,7 @@ func TestGatewayF3(t *testing.T) {
t.Run("disabled", func(t *testing.T) { t.Run("disabled", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
nodes := startNodes(ctx, t, withNodeOpts(kit.F3Enabled(nil))) nodes := startNodes(ctx, t, withNodeOpts(kit.F3Disabled()))
cert, err := nodes.lite.F3GetLatestCertificate(ctx) cert, err := nodes.lite.F3GetLatestCertificate(ctx)
require.ErrorIs(t, err, api.ErrF3Disabled) require.ErrorIs(t, err, api.ErrF3Disabled)

View File

@ -447,6 +447,7 @@ func (n *Ensemble) Start() *Ensemble {
opts := []node.Option{ opts := []node.Option{
node.FullAPI(&full.FullNode, node.Lite(full.options.lite)), node.FullAPI(&full.FullNode, node.Lite(full.options.lite)),
node.FullAPIv2(&full.V2),
node.Base(), node.Base(),
node.Repo(r), node.Repo(r),
node.If(full.options.disableLibp2p, node.MockHost(n.mn)), node.If(full.options.disableLibp2p, node.MockHost(n.mn)),

View File

@ -20,6 +20,7 @@ import (
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v1api" "github.com/filecoin-project/lotus/api/v1api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/wallet/key" "github.com/filecoin-project/lotus/chain/wallet/key"
cliutil "github.com/filecoin-project/lotus/cli/util" cliutil "github.com/filecoin-project/lotus/cli/util"
@ -35,6 +36,7 @@ type Libp2p struct {
// TestFullNode represents a full node enrolled in an Ensemble. // TestFullNode represents a full node enrolled in an Ensemble.
type TestFullNode struct { type TestFullNode struct {
v1api.FullNode v1api.FullNode
V2 v2api.FullNode
t *testing.T t *testing.T

View File

@ -247,20 +247,28 @@ func MutateSealingConfig(mut func(sc *config.SealingConfig)) NodeOpt {
}))) })))
} }
// F3Enabled enables the F3 feature in the node. If the provided config is nil, // F3Config sets the F3 configuration to be used by test node.
// the feature is disabled. func F3Config(cfg *lf3.Config) NodeOpt {
func F3Enabled(cfg *lf3.Config) NodeOpt {
if cfg == nil {
return ConstructorOpts(
node.Unset(new(*lf3.Config)),
node.Unset(new(manifest.ManifestProvider)),
node.Unset(new(*lf3.F3)),
)
}
return ConstructorOpts( return ConstructorOpts(
node.Override(new(*lf3.Config), cfg), node.Override(new(*lf3.Config), cfg),
node.Override(new(manifest.ManifestProvider), lf3.NewManifestProvider), )
node.Override(new(*lf3.F3), lf3.New), }
// F3Backend overrides the F3 backend implementation used by test node.
func F3Backend(backend lf3.F3Backend) NodeOpt {
return ConstructorOpts(
node.Override(new(lf3.F3Backend), backend),
)
}
// F3Disabled disables the F3 subsystem for this node.
func F3Disabled() NodeOpt {
return ConstructorOpts(
node.Unset(new(*lf3.Config)),
node.Unset(new(*lf3.ContractManifestProvider)),
node.Unset(new(lf3.StateCaller)),
node.Unset(new(manifest.ManifestProvider)),
node.Unset(new(lf3.F3Backend)),
) )
} }

View File

@ -1,8 +1,10 @@
package kit package kit
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -44,7 +46,7 @@ func CreateRPCServer(t *testing.T, handler http.Handler, listener net.Listener)
} }
func fullRpc(t *testing.T, f *TestFullNode) (*TestFullNode, Closer) { func fullRpc(t *testing.T, f *TestFullNode) (*TestFullNode, Closer) {
handler, err := node.FullNodeHandler(f.FullNode, false) handler, err := node.FullNodeHandler(f.FullNode, f.V2, false)
require.NoError(t, err) require.NoError(t, err)
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := net.Listen("tcp", "127.0.0.1:0")
@ -105,3 +107,19 @@ func workerRpc(t *testing.T, m *TestWorker) *TestWorker {
m.ListenAddr, m.Worker = maddr, cl m.ListenAddr, m.Worker = maddr, cl
return m return m
} }
func (full *TestFullNode) DoRawRPCRequest(t *testing.T, version int, body string) (int, []byte) {
t.Helper()
require.NotEmpty(t, full.ListenURL, "not listening for rpc, turn on with `kit.ThroughRPC()`")
endpoint := fmt.Sprintf("%s/rpc/v%d", full.ListenURL, version)
request, err := http.NewRequest("POST", endpoint, bytes.NewReader([]byte(body)))
require.NoError(t, err)
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
require.NoError(t, err)
defer func() { require.NoError(t, response.Body.Close()) }()
respBody, err := io.ReadAll(response.Body)
require.NoError(t, err)
return response.StatusCode, respBody
}

View File

@ -9,6 +9,7 @@ import (
"go.opencensus.io/tag" "go.opencensus.io/tag"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/metrics"
) )
@ -30,6 +31,12 @@ func MetricedFullAPI(a api.FullNode) api.FullNode {
return &out return &out
} }
func MetricedFullV2API(a v2api.FullNode) v2api.FullNode {
var out v2api.FullNodeStruct
proxy(a, &out)
return &out
}
func MetricedWorkerAPI(a api.Worker) api.Worker { func MetricedWorkerAPI(a api.Worker) api.Worker {
var out api.WorkerStruct var out api.WorkerStruct
proxy(a, &out) proxy(a, &out)

View File

@ -136,6 +136,7 @@ const (
StoreEventsKey StoreEventsKey
InitChainIndexerKey InitChainIndexerKey
InitApiV2Key
_nInvokes // keep this last _nInvokes // keep this last
) )

View File

@ -12,6 +12,7 @@ import (
"github.com/filecoin-project/go-f3/manifest" "github.com/filecoin-project/go-f3/manifest"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain" "github.com/filecoin-project/lotus/chain"
"github.com/filecoin-project/lotus/chain/beacon" "github.com/filecoin-project/lotus/chain/beacon"
@ -136,6 +137,7 @@ var ChainNode = Options(
Override(new(messagepool.Provider), messagepool.NewProviderLite), Override(new(messagepool.Provider), messagepool.NewProviderLite),
Override(new(messagepool.MpoolNonceAPI), From(new(modules.MpoolNonceAPI))), Override(new(messagepool.MpoolNonceAPI), From(new(modules.MpoolNonceAPI))),
Override(new(full.ChainModuleAPI), From(new(api.Gateway))), Override(new(full.ChainModuleAPI), From(new(api.Gateway))),
Override(new(full.ChainModuleAPIv2), From(new(full.ChainModuleV2))),
Override(new(full.GasModuleAPI), From(new(api.Gateway))), Override(new(full.GasModuleAPI), From(new(api.Gateway))),
Override(new(full.MpoolModuleAPI), From(new(api.Gateway))), Override(new(full.MpoolModuleAPI), From(new(api.Gateway))),
Override(new(full.StateModuleAPI), From(new(api.Gateway))), Override(new(full.StateModuleAPI), From(new(api.Gateway))),
@ -162,6 +164,7 @@ var ChainNode = Options(
Override(new(messagepool.Provider), messagepool.NewProvider), Override(new(messagepool.Provider), messagepool.NewProvider),
Override(new(messagepool.MpoolNonceAPI), From(new(*messagepool.MessagePool))), Override(new(messagepool.MpoolNonceAPI), From(new(*messagepool.MessagePool))),
Override(new(full.ChainModuleAPI), From(new(full.ChainModule))), Override(new(full.ChainModuleAPI), From(new(full.ChainModule))),
Override(new(full.ChainModuleAPIv2), From(new(full.ChainModuleV2))),
Override(new(full.GasModuleAPI), From(new(full.GasModule))), Override(new(full.GasModuleAPI), From(new(full.GasModule))),
Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))), Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))),
Override(new(full.StateModuleAPI), From(new(full.StateModule))), Override(new(full.StateModuleAPI), From(new(full.StateModule))),
@ -179,7 +182,7 @@ var ChainNode = Options(
Override(new(*lf3.ContractManifestProvider), lf3.NewContractManifestProvider), Override(new(*lf3.ContractManifestProvider), lf3.NewContractManifestProvider),
Override(new(lf3.StateCaller), From(new(full.StateModule))), Override(new(lf3.StateCaller), From(new(full.StateModule))),
Override(new(manifest.ManifestProvider), lf3.NewManifestProvider), Override(new(manifest.ManifestProvider), lf3.NewManifestProvider),
Override(new(*lf3.F3), lf3.New), Override(new(lf3.F3Backend), lf3.New),
), ),
) )
@ -363,3 +366,12 @@ func FullAPI(out *api.FullNode, fopts ...FullOption) Option {
}, },
) )
} }
func FullAPIv2(out *v2api.FullNode) Option {
return func(s *Settings) error {
resAPI := &impl.FullNodeAPIv2{}
s.invokes[InitApiV2Key] = fx.Populate(resAPI)
*out = resAPI
return nil
}
}

View File

@ -6,8 +6,10 @@ import (
logging "github.com/ipfs/go-log/v2" logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"go.uber.org/fx"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/actors/policy" "github.com/filecoin-project/lotus/chain/actors/policy"
"github.com/filecoin-project/lotus/node/impl/common" "github.com/filecoin-project/lotus/node/impl/common"
@ -121,3 +123,11 @@ func (n *FullNodeAPI) NodeStatus(ctx context.Context, inclChainStatus bool) (sta
} }
var _ api.FullNode = &FullNodeAPI{} var _ api.FullNode = &FullNodeAPI{}
type FullNodeAPIv2 struct {
fx.In
full.ChainModuleAPIv2
}
var _ v2api.FullNode = &FullNodeAPIv2{}

120
node/impl/full/chain_v2.go Normal file
View File

@ -0,0 +1,120 @@
package full
import (
"context"
"errors"
"go.uber.org/fx"
"golang.org/x/xerrors"
"github.com/filecoin-project/go-f3"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/actors/policy"
"github.com/filecoin-project/lotus/chain/lf3"
"github.com/filecoin-project/lotus/chain/store"
"github.com/filecoin-project/lotus/chain/types"
)
type ChainModuleAPIv2 interface {
ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error)
}
type ChainModuleV2 struct {
Chain *store.ChainStore
F3 lf3.F3Backend `optional:"true"`
fx.In
}
var _ ChainModuleAPIv2 = (*ChainModuleV2)(nil)
func (cm *ChainModuleV2) ChainGetTipSet(ctx context.Context, selector types.TipSetSelector) (*types.TipSet, error) {
if err := selector.Validate(); err != nil {
return nil, xerrors.Errorf("validating selector: %w", err)
}
// Get tipset by key.
if selector.Key != nil {
return cm.Chain.GetTipSetFromKey(ctx, *selector.Key)
}
// Get tipset by height.
if selector.Height != nil {
anchor, err := cm.getTipSetByAnchor(ctx, selector.Height.Anchor)
if err != nil {
return nil, xerrors.Errorf("getting anchor from tipset: %w", err)
}
return cm.Chain.GetTipsetByHeight(ctx, *selector.Height.At, anchor, selector.Height.Previous)
}
// Get tipset by tag, either latest or finalized.
if selector.Tag != nil {
return cm.getTipSetByTag(ctx, *selector.Tag)
}
return nil, xerrors.Errorf("no tipset found for selector")
}
func (cm *ChainModuleV2) getTipSetByTag(ctx context.Context, tag types.TipSetTag) (*types.TipSet, error) {
if tag == types.TipSetTags.Latest {
return cm.Chain.GetHeaviestTipSet(), nil
}
if tag != types.TipSetTags.Finalized {
return nil, xerrors.Errorf("unknown tipset tag: %s", tag)
}
if cm.F3 == nil {
// F3 is disabled; fall back to EC finality.
return cm.getECFinalized(ctx)
}
cert, err := cm.F3.GetLatestCert(ctx)
if err != nil {
if errors.Is(err, f3.ErrF3NotRunning) || errors.Is(err, api.ErrF3NotReady) {
// Only fall back to EC finality if F3 isn't running or not ready.
log.Debugw("F3 not running or not ready, falling back to EC finality", "err", err)
return cm.getECFinalized(ctx)
}
return nil, err
}
if cert == nil {
// No latest certificate. Fall back to EC finality.
return cm.getECFinalized(ctx)
}
// Extract the finalized tipeset from the certificate.
tsk, err := types.TipSetKeyFromBytes(cert.ECChain.Head().Key)
if err != nil {
return nil, xerrors.Errorf("decoding latest f3 cert tipset key: %w", err)
}
ts, err := cm.Chain.LoadTipSet(ctx, tsk)
if err != nil {
return nil, xerrors.Errorf("loading latest f3 cert tipset %s: %w", tsk, err)
}
return ts, nil
}
func (cm *ChainModuleV2) getTipSetByAnchor(ctx context.Context, anchor *types.TipSetAnchor) (*types.TipSet, error) {
switch {
case anchor == nil:
// No anchor specified. Fall back to finalized tipset.
return cm.getTipSetByTag(ctx, types.TipSetTags.Finalized)
case anchor.Key == nil && anchor.Tag == nil:
// Anchor is zero-valued. Fall back to heaviest tipset.
return cm.Chain.GetHeaviestTipSet(), nil
case anchor.Key != nil:
// Get tipset at the specified key.
return cm.Chain.GetTipSetFromKey(ctx, *anchor.Key)
case anchor.Tag != nil:
return cm.getTipSetByTag(ctx, *anchor.Tag)
default:
return nil, xerrors.Errorf("invalid anchor: %v", anchor)
}
}
func (cm *ChainModuleV2) getECFinalized(ctx context.Context) (*types.TipSet, error) {
head := cm.Chain.GetHeaviestTipSet()
finalizedHeight := head.Height() - policy.ChainFinality
return cm.Chain.GetTipsetByHeight(ctx, finalizedHeight, head, true)
}

View File

@ -19,7 +19,7 @@ import (
type F3API struct { type F3API struct {
fx.In fx.In
F3 *lf3.F3 `optional:"true"` F3 lf3.F3Backend `optional:"true"`
} }
func (f3api *F3API) F3GetOrRenewParticipationTicket(ctx context.Context, miner address.Address, previous api.F3ParticipationTicket, instances uint64) (api.F3ParticipationTicket, error) { func (f3api *F3API) F3GetOrRenewParticipationTicket(ctx context.Context, miner address.Address, previous api.F3ParticipationTicket, instances uint64) (api.F3ParticipationTicket, error) {

View File

@ -22,6 +22,7 @@ import (
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v0api" "github.com/filecoin-project/lotus/api/v0api"
"github.com/filecoin-project/lotus/api/v1api" "github.com/filecoin-project/lotus/api/v1api"
"github.com/filecoin-project/lotus/api/v2api"
"github.com/filecoin-project/lotus/lib/rpcenc" "github.com/filecoin-project/lotus/lib/rpcenc"
"github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/metrics"
"github.com/filecoin-project/lotus/metrics/proxy" "github.com/filecoin-project/lotus/metrics/proxy"
@ -66,7 +67,7 @@ func ServeRPC(h http.Handler, id string, addr multiaddr.Multiaddr) (StopFunc, er
} }
// FullNodeHandler returns a full node handler, to be mounted as-is on the server. // FullNodeHandler returns a full node handler, to be mounted as-is on the server.
func FullNodeHandler(a v1api.FullNode, permissioned bool, opts ...jsonrpc.ServerOption) (http.Handler, error) { func FullNodeHandler(v1 v1api.FullNode, v2 v2api.FullNode, permissioned bool, opts ...jsonrpc.ServerOption) (http.Handler, error) {
m := mux.NewRouter() m := mux.NewRouter()
serveRpc := func(path string, hnd interface{}) { serveRpc := func(path string, hnd interface{}) {
@ -78,34 +79,40 @@ func FullNodeHandler(a v1api.FullNode, permissioned bool, opts ...jsonrpc.Server
var handler http.Handler = rpcServer var handler http.Handler = rpcServer
if permissioned { if permissioned {
handler = &auth.Handler{Verify: a.AuthVerify, Next: rpcServer.ServeHTTP} handler = &auth.Handler{Verify: v1.AuthVerify, Next: rpcServer.ServeHTTP}
} }
m.Handle(path, handler) m.Handle(path, handler)
} }
fnapi := proxy.MetricedFullAPI(a) v1Proxy := proxy.MetricedFullAPI(v1)
v2Proxy := proxy.MetricedFullV2API(v2)
if permissioned { if permissioned {
fnapi = api.PermissionedFullAPI(fnapi) v1Proxy = api.PermissionedFullAPI(v1Proxy)
v2Proxy = v2api.PermissionedFullAPI(v2Proxy)
} }
v0Proxy := &v0api.WrapperV1Full{FullNode: v1Proxy}
var v0 v0api.FullNode = &(struct{ v0api.FullNode }{&v0api.WrapperV1Full{FullNode: fnapi}}) serveRpc("/rpc/v1", v1Proxy)
serveRpc("/rpc/v1", fnapi) serveRpc("/rpc/v2", v2Proxy)
serveRpc("/rpc/v0", v0) serveRpc("/rpc/v0", v0Proxy)
// debugging // debugging
m.Handle("/debug/metrics", metrics.Exporter()) m.Handle("/debug/metrics", metrics.Exporter())
m.Handle("/debug/pprof-set/block", handleFractionOpt("BlockProfileRate", runtime.SetBlockProfileRate)) m.Handle("/debug/pprof-set/block", handleFractionOpt("BlockProfileRate", runtime.SetBlockProfileRate))
m.Handle("/debug/pprof-set/mutex", handleFractionOpt("MutexProfileFraction", func(x int) { m.Handle("/debug/pprof-set/mutex", handleFractionOpt("MutexProfileFraction", setMutexProfileFraction))
runtime.SetMutexProfileFraction(x) m.Handle("/health/livez", NewLiveHandler(v1))
})) m.Handle("/health/readyz", NewReadyHandler(v1))
m.Handle("/health/livez", NewLiveHandler(a))
m.Handle("/health/readyz", NewReadyHandler(a))
m.PathPrefix("/").Handler(http.DefaultServeMux) // pprof m.PathPrefix("/").Handler(http.DefaultServeMux) // pprof
return m, nil return m, nil
} }
func setMutexProfileFraction(to int) {
from := runtime.SetMutexProfileFraction(to)
log.Debugw("Mutex profile fraction rate is set", "from", from, "to", to)
}
// MinerHandler returns a miner handler, to be mounted as-is on the server. // MinerHandler returns a miner handler, to be mounted as-is on the server.
func MinerHandler(a api.StorageMiner, permissioned bool) (http.Handler, error) { func MinerHandler(a api.StorageMiner, permissioned bool) (http.Handler, error) {
mapi := proxy.MetricedStorMinerAPI(a) mapi := proxy.MetricedStorMinerAPI(a)