diff --git a/CHANGELOG.md b/CHANGELOG.md index 118d8c35526..60a1fd45808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,13 @@ # UNRELEASED - 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) - feat: add gas to application metric reporting `vm/applyblocks_early_gas`, `vm/applyblocks_messages_gas`, `vm/applyblocks_cron_gas` ([filecoin-project/lotus#13030](https://github.com/filecoin-project/lotus/pull/13030)) +- feat: add Lotus v2 experimental APIs with F3 awareness + The Lotus V2 APIs introduce a powerful new TipSet selection mechanism that significantly enhances how applications interact with the Filecoin blockchain. The design reduces API footprint, seamlessly handles both traditional Expected Consensus and the new F3 protocol, and provides graceful fallbacks. See [Filecoin v2 APIs](https://filoznotebook.notion.site/Filecoin-V2-APIs-1d0dc41950c1808b914de5966d501658) for an + in-depth overview. + Pull requests: + - https://github.com/filecoin-project/lotus/pull/13003 + - https://github.com/filecoin-project/lotus/pull/13027 # Node and Miner v1.32.2 / 2025-04-04 @@ -66,7 +71,7 @@ The Lotus v1.32.1 release is a **MANDATORY patch release**, which will deliver t - The minimum supported Golang version is now `1.23.6` ([filecoin-project/lotus#12910](https://github.com/filecoin-project/lotus/pull/12910)). - The `SupportedProofTypes` field has been removed from the `Filecoin.StateGetNetworkParams` method because it was frequently overlooked during proof type updates and did not accurately reflect the FVM's supported proofs ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881)). - Introduced `Agent` field to the `Filecoin.Version` response. Note that this change may be breaking, depending on the clients deserialization capabilities. ([filecoin-project/lotus#12904](https://github.com/filecoin-project/lotus/pull/12904)). -- The `--only-cc` option has been removed from the `lotus-miner sectors extend` command. +- The `--only-cc` option has been removed from the `lotus-miner sectors extend` command. ## ๐Ÿ›๏ธ Filecoin network version 25 FIPs @@ -120,7 +125,7 @@ For certain node operators, such as full archival nodes or systems that need to - chore: switch to pure-go zstd decoder for snapshot imports. ([filecoin-project/lotus#12857](https://github.com/filecoin-project/lotus/pull/12857)) - chore: upgrade go-state-types with big.Int{} change that means an empty big.Int is now treated as zero for all operations ([filecoin-project/lotus#12936](https://github.com/filecoin-project/lotus/pull/12936)) - chore(eth): make `EthGetBlockByNumber` & `EthGetBlockByHash` share the same cache and be impacted by `EthBlkCacheSize` config settings ([filecoin-project/lotus#12979](https://github.com/filecoin-project/lotus/pull/12979)) -- chore(deps): bump go-state-types to v0.16.0-rc8 ([filecoin-project/lotus#12973](https://github.com/filecoin-project/lotus/pull/12973)) +- chore(deps): bump go-state-types to v0.16.0-rc8 ([filecoin-project/lotus#12973](https://github.com/filecoin-project/lotus/pull/12973)) - chore: set Mainnet nv25 upgrade epoch and update deps ([filecoin-project/lotus#12986](https://github.com/filecoin-project/lotus/pull/12986)) - chore(eth): make EthGetBlockByNumber & EthGetBlockByHash share cache code ([filecoin-project/lotus#12979](https://github.com/filecoin-project/lotus/pull/12979)) @@ -186,7 +191,7 @@ This is the stable release of the **upcoming MANDATORY Lotus v1.32.0 release**, - The minimum supported Golang version is now `1.23.6` ([filecoin-project/lotus#12910](https://github.com/filecoin-project/lotus/pull/12910)). - The `SupportedProofTypes` field has been removed from the `Filecoin.StateGetNetworkParams` method because it was frequently overlooked during proof type updates and did not accurately reflect the FVM's supported proofs ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881)). - Introduced `Agent` field to the `Filecoin.Version` response. Note that this change may be breaking, depending on the clients deserialization capabilities. ([filecoin-project/lotus#12904](https://github.com/filecoin-project/lotus/pull/12904)). -- The `--only-cc` option has been removed from the `lotus-miner sectors extend` command. +- The `--only-cc` option has been removed from the `lotus-miner sectors extend` command. ## ๐Ÿ›๏ธ Filecoin network version 25 FIPs @@ -239,7 +244,7 @@ For certain node operators, such as full archival nodes or systems that need to - chore: switch to pure-go zstd decoder for snapshot imports. ([filecoin-project/lotus#12857](https://github.com/filecoin-project/lotus/pull/12857)) - chore: upgrade go-state-types with big.Int{} change that means an empty big.Int is now treated as zero for all operations ([filecoin-project/lotus#12936](https://github.com/filecoin-project/lotus/pull/12936)) - chore(eth): make `EthGetBlockByNumber` & `EthGetBlockByHash` share the same cache and be impacted by `EthBlkCacheSize` config settings ([filecoin-project/lotus#12979](https://github.com/filecoin-project/lotus/pull/12979)) -- chore(deps): bump go-state-types to v0.16.0-rc8 ([filecoin-project/lotus#12973](https://github.com/filecoin-project/lotus/pull/12973)) +- chore(deps): bump go-state-types to v0.16.0-rc8 ([filecoin-project/lotus#12973](https://github.com/filecoin-project/lotus/pull/12973)) - chore: set Mainnet nv25 upgrade epoch and update deps ([filecoin-project/lotus#12986](https://github.com/filecoin-project/lotus/pull/12986)) - chore(eth): make EthGetBlockByNumber & EthGetBlockByHash share cache code ([filecoin-project/lotus#12979](https://github.com/filecoin-project/lotus/pull/12979)) @@ -420,8 +425,8 @@ This is the final release of the MANDATORY Lotus v1.30.0 release, which delivers - If you are running the v1.28.x version of Lotus, please go through the Upgrade Warnings section for the v1.28.* releases and v1.29.*, before upgrading to this release. - This release requires a minimum Go version of v1.22.7 or higher. - The `releases` branch has been deprecated with the 202408 split of 'Lotus Node' and 'Lotus Miner'. See https://github.com/filecoin-project/lotus/blob/master/LOTUS_RELEASE_FLOW.md#why-is-the-releases-branch-deprecated-and-what-are-alternatives for more info and alternatives for getting the latest release for both the 'Lotus Node' and 'Lotus Miner' based on the Branch and Tag Strategy. - - To get the latest Lotus Node tag: git tag -l 'v*' | sort -V -r | head -n 1 - - To get the latest Lotus Miner tag: git tag -l 'miner/v*' | sort -V -r | head -n 1 + - To get the latest Lotus Node tag: git tag -l 'v*' | sort -V -r | head -n 1 + - To get the latest Lotus Miner tag: git tag -l 'miner/v*' | sort -V -r | head -n 1 ## ๐Ÿ›๏ธ Filecoin network version 24 FIPs @@ -532,4 +537,4 @@ For the set of changes since the last stable release: | DemoYeti | 1 | +2/-1 | 1 | | qwdsds | 1 | +1/-1 | 1 | | Samuel Arogbonlo | 1 | +2/-0 | 2 | -| Elias Rad | 1 | +1/-1 | 1 | +| Elias Rad | 1 | +1/-1 | 1 | \ No newline at end of file diff --git a/api/v2api/full.go b/api/v2api/full.go index d1888b50ba9..244c2dbb219 100644 --- a/api/v2api/full.go +++ b/api/v2api/full.go @@ -3,6 +3,8 @@ package v2api import ( "context" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/lotus/chain/types" ) @@ -49,4 +51,51 @@ type FullNode interface { // fmt.Printf("Latest TipSet: %v\n", tipSet) // ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error) //perm:read + + // MethodGroup: State + // The State method group contains methods for interacting with the Filecoin + // blockchain state, including actor information, addresses, and chain data. + // These methods allow querying the blockchain state at any point in its history + // using flexible TipSet selection mechanisms. + + // StateGetActor retrieves the actor information for the specified address at the + // selected tipset. + // + // This function returns the on-chain Actor object including: + // - Code CID (determines the actor's type) + // - State root CID + // - Balance in attoFIL + // - Nonce (for account actors) + // + // The TipSetSelector parameter provides flexible options for selecting the tipset: + // - TipSetSelectors.Latest: the most recent tipset with the heaviest weight + // - TipSetSelectors.Finalized: the most recent finalized tipset + // - TipSetSelectors.Height(epoch, previous, anchor): tipset at the specified height + // - TipSetSelectors.Key(key): tipset with the specified key + // + // See types.TipSetSelector documentation for additional details. + // + // If the actor does not exist at the specified tipset, this function returns nil. + // + // Experimental: This API is experimental and may change without notice. + StateGetActor(context.Context, address.Address, types.TipSetSelector) (*types.Actor, error) //perm:read + + // StateGetID retrieves the ID address for the specified address at the selected tipset. + // + // Every actor on the Filecoin network has a unique ID address (format: f0123). + // This function resolves any address type (ID, robust, or delegated) to its canonical + // ID address representation at the specified tipset. + // + // The function is particularly useful for: + // - Normalizing different address formats to a consistent representation + // - Following address changes across state transitions + // - Verifying that an address corresponds to an existing actor + // + // The TipSetSelector parameter provides flexible options for selecting the tipset. + // See StateGetActor documentation for details on selection options. + // + // If the address cannot be resolved at the specified tipset, this function returns nil. + // + // Experimental: This API is experimental and may change without notice. + StateGetID(context.Context, address.Address, types.TipSetSelector) (*address.Address, error) //perm:read } diff --git a/api/v2api/proxy_gen.go b/api/v2api/proxy_gen.go index 7ccbffd2b9a..eaba1273c3f 100644 --- a/api/v2api/proxy_gen.go +++ b/api/v2api/proxy_gen.go @@ -7,6 +7,8 @@ import ( "golang.org/x/xerrors" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/lotus/chain/types" ) @@ -18,6 +20,10 @@ type FullNodeStruct struct { type FullNodeMethods struct { ChainGetTipSet func(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) `perm:"read"` + + StateGetActor func(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*types.Actor, error) `perm:"read"` + + StateGetID func(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*address.Address, error) `perm:"read"` } type FullNodeStub struct { @@ -34,4 +40,26 @@ func (s *FullNodeStub) ChainGetTipSet(p0 context.Context, p1 types.TipSetSelecto return nil, ErrNotSupported } +func (s *FullNodeStruct) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*types.Actor, error) { + if s.Internal.StateGetActor == nil { + return nil, ErrNotSupported + } + return s.Internal.StateGetActor(p0, p1, p2) +} + +func (s *FullNodeStub) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*types.Actor, error) { + return nil, ErrNotSupported +} + +func (s *FullNodeStruct) StateGetID(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*address.Address, error) { + if s.Internal.StateGetID == nil { + return nil, ErrNotSupported + } + return s.Internal.StateGetID(p0, p1, p2) +} + +func (s *FullNodeStub) StateGetID(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*address.Address, error) { + return nil, ErrNotSupported +} + var _ FullNode = new(FullNodeStruct) diff --git a/api/v2api/v2mocks/mock_full.go b/api/v2api/v2mocks/mock_full.go index e108bcb0726..fd790e29139 100644 --- a/api/v2api/v2mocks/mock_full.go +++ b/api/v2api/v2mocks/mock_full.go @@ -10,6 +10,8 @@ import ( gomock "github.com/golang/mock/gomock" + address "github.com/filecoin-project/go-address" + types "github.com/filecoin-project/lotus/chain/types" ) @@ -50,3 +52,33 @@ func (mr *MockFullNodeMockRecorder) ChainGetTipSet(arg0, arg1 interface{}) *gomo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainGetTipSet", reflect.TypeOf((*MockFullNode)(nil).ChainGetTipSet), arg0, arg1) } + +// StateGetActor mocks base method. +func (m *MockFullNode) StateGetActor(arg0 context.Context, arg1 address.Address, arg2 types.TipSetSelector) (*types.ActorV5, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StateGetActor", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.ActorV5) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StateGetActor indicates an expected call of StateGetActor. +func (mr *MockFullNodeMockRecorder) StateGetActor(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateGetActor", reflect.TypeOf((*MockFullNode)(nil).StateGetActor), arg0, arg1, arg2) +} + +// StateGetID mocks base method. +func (m *MockFullNode) StateGetID(arg0 context.Context, arg1 address.Address, arg2 types.TipSetSelector) (*address.Address, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StateGetID", arg0, arg1, arg2) + ret0, _ := ret[0].(*address.Address) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StateGetID indicates an expected call of StateGetID. +func (mr *MockFullNodeMockRecorder) StateGetID(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateGetID", reflect.TypeOf((*MockFullNode)(nil).StateGetID), arg0, arg1, arg2) +} diff --git a/build/openrpc/v2/full.json b/build/openrpc/v2/full.json index 17eafbeac33..7f30347adfe 100644 --- a/build/openrpc/v2/full.json +++ b/build/openrpc/v2/full.json @@ -152,7 +152,235 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L26" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L32" + } + }, + { + "name": "Filecoin.StateGetActor", + "description": "```go\nfunc (s *FullNodeStruct) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*types.Actor, error) {\n\tif s.Internal.StateGetActor == nil {\n\t\treturn nil, ErrNotSupported\n\t}\n\treturn s.Internal.StateGetActor(p0, p1, p2)\n}\n```", + "summary": "StateGetActor retrieves the actor information for the specified address at the\nselected tipset.\n\nThis function returns the on-chain Actor object including:\n - Code CID (determines the actor's type)\n - State root CID\n - Balance in attoFIL\n - Nonce (for account actors)\n\nThe TipSetSelector parameter provides flexible options for selecting the tipset:\n - TipSetSelectors.Latest: the most recent tipset with the heaviest weight\n - TipSetSelectors.Finalized: the most recent finalized tipset\n - TipSetSelectors.Height(epoch, previous, anchor): tipset at the specified height\n - TipSetSelectors.Key(key): tipset with the specified key\n\nSee types.TipSetSelector documentation for additional details.\n\nIf the actor does not exist at the specified tipset, this function returns nil.\n\nExperimental: This API is experimental and may change without notice.\n", + "paramStructure": "by-position", + "params": [ + { + "name": "p1", + "description": "address.Address", + "summary": "", + "schema": { + "examples": [ + "f01234" + ], + "additionalProperties": false, + "type": [ + "object" + ] + }, + "required": true, + "deprecated": false + }, + { + "name": "p2", + "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.Actor", + "description": "*types.Actor", + "summary": "", + "schema": { + "examples": [ + { + "Code": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + }, + "Head": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + }, + "Nonce": 42, + "Balance": "0", + "DelegatedAddress": "f01234" + } + ], + "additionalProperties": false, + "properties": { + "Balance": { + "additionalProperties": false, + "type": "object" + }, + "Code": { + "title": "Content Identifier", + "type": "string" + }, + "DelegatedAddress": { + "additionalProperties": false, + "type": "object" + }, + "Head": { + "title": "Content Identifier", + "type": "string" + }, + "Nonce": { + "title": "number", + "type": "number" + } + }, + "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#L43" + } + }, + { + "name": "Filecoin.StateGetID", + "description": "```go\nfunc (s *FullNodeStruct) StateGetID(p0 context.Context, p1 address.Address, p2 types.TipSetSelector) (*address.Address, error) {\n\tif s.Internal.StateGetID == nil {\n\t\treturn nil, ErrNotSupported\n\t}\n\treturn s.Internal.StateGetID(p0, p1, p2)\n}\n```", + "summary": "StateGetID retrieves the ID address for the specified address at the selected tipset.\n\nEvery actor on the Filecoin network has a unique ID address (format: f0123).\nThis function resolves any address type (ID, robust, or delegated) to its canonical\nID address representation at the specified tipset.\n\nThe function is particularly useful for:\n - Normalizing different address formats to a consistent representation\n - Following address changes across state transitions\n - Verifying that an address corresponds to an existing actor\n\nThe TipSetSelector parameter provides flexible options for selecting the tipset.\nSee StateGetActor documentation for details on selection options.\n\nIf the address cannot be resolved at the specified tipset, this function returns nil.\n\nExperimental: This API is experimental and may change without notice.\n", + "paramStructure": "by-position", + "params": [ + { + "name": "p1", + "description": "address.Address", + "summary": "", + "schema": { + "examples": [ + "f01234" + ], + "additionalProperties": false, + "type": [ + "object" + ] + }, + "required": true, + "deprecated": false + }, + { + "name": "p2", + "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": "*address.Address", + "description": "*address.Address", + "summary": "", + "schema": { + "examples": [ + "f01234" + ], + "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#L54" } } ] diff --git a/documentation/en/api-v2-unstable-methods.md b/documentation/en/api-v2-unstable-methods.md index ac00db149d8..41d43cb951e 100644 --- a/documentation/en/api-v2-unstable-methods.md +++ b/documentation/en/api-v2-unstable-methods.md @@ -1,6 +1,9 @@ # Groups * [Chain](#Chain) * [ChainGetTipSet](#ChainGetTipSet) +* [State](#State) + * [StateGetActor](#StateGetActor) + * [StateGetID](#StateGetID) ## Chain The Chain method group contains methods for interacting with the blockchain. @@ -124,3 +127,94 @@ Response: } ``` +## State +The State method group contains methods for interacting with the Filecoin +blockchain state, including actor information, addresses, and chain data. +These methods allow querying the blockchain state at any point in its history +using flexible TipSet selection mechanisms. + + +### StateGetActor +StateGetActor retrieves the actor information for the specified address at the +selected tipset. + +This function returns the on-chain Actor object including: + - Code CID (determines the actor's type) + - State root CID + - Balance in attoFIL + - Nonce (for account actors) + +The TipSetSelector parameter provides flexible options for selecting the tipset: + - TipSetSelectors.Latest: the most recent tipset with the heaviest weight + - TipSetSelectors.Finalized: the most recent finalized tipset + - TipSetSelectors.Height(epoch, previous, anchor): tipset at the specified height + - TipSetSelectors.Key(key): tipset with the specified key + +See types.TipSetSelector documentation for additional details. + +If the actor does not exist at the specified tipset, this function returns nil. + +Experimental: This API is experimental and may change without notice. + + +Perms: read + +Inputs: +```json +[ + "f01234", + { + "tag": "finalized" + } +] +``` + +Response: +```json +{ + "Code": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + }, + "Head": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + }, + "Nonce": 42, + "Balance": "0", + "DelegatedAddress": "f01234" +} +``` + +### StateGetID +StateGetID retrieves the ID address for the specified address at the selected tipset. + +Every actor on the Filecoin network has a unique ID address (format: f0123). +This function resolves any address type (ID, robust, or delegated) to its canonical +ID address representation at the specified tipset. + +The function is particularly useful for: + - Normalizing different address formats to a consistent representation + - Following address changes across state transitions + - Verifying that an address corresponds to an existing actor + +The TipSetSelector parameter provides flexible options for selecting the tipset. +See StateGetActor documentation for details on selection options. + +If the address cannot be resolved at the specified tipset, this function returns nil. + +Experimental: This API is experimental and may change without notice. + + +Perms: read + +Inputs: +```json +[ + "f01234", + { + "tag": "finalized" + } +] +``` + +Response: `"f01234"` + diff --git a/itests/api_v2_test.go b/itests/api_v2_test.go index cb072b8c357..701bd8fcb1c 100644 --- a/itests/api_v2_test.go +++ b/itests/api_v2_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-f3" "github.com/filecoin-project/go-f3/certs" "github.com/filecoin-project/go-f3/gpbft" @@ -24,7 +25,7 @@ import ( "github.com/filecoin-project/lotus/itests/kit" ) -func TestAPIV2_GetTipSetThroughRPC(t *testing.T) { +func TestAPIV2_ThroughRPC(t *testing.T) { const ( timeout = 2 * time.Minute blockTime = 10 * time.Millisecond @@ -36,7 +37,7 @@ func TestAPIV2_GetTipSetThroughRPC(t *testing.T) { kit.QuietMiningLogs() mockF3 := newMockF3Backend() - subject, _, network := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.F3Backend(mockF3)) + subject, miner, network := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.F3Backend(mockF3)) network.BeginMining(blockTime) subject.WaitTillChain(ctx, kit.HeightAtLeast(targetHeight)) @@ -84,187 +85,341 @@ func TestAPIV2_GetTipSetThroughRPC(t *testing.T) { // 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 + // Layer 7 of the ISO model. + + t.Run("ChainGetTipSet", func(t *testing.T) { + 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, }, - 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) + { + name: "latest tag is ok", + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"latest"}],"id":1}`, + wantTipSet: heaviest, + wantResponseStatus: http.StatusOK, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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 + { + name: "height with no epoch", + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{}}],"id":1}`, + wantErr: "epoch must be specified", + wantResponseStatus: http.StatusOK, }, - 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 + { + 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, }, - 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 + { + 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, }, - 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) + { + 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) + } + }) + } + }) + t.Run("StateGetActor", func(t *testing.T) { + v1StateGetActor := func(t *testing.T, ts func(*testing.T) *types.TipSet) func(*testing.T) *types.Actor { + return func(t *testing.T) *types.Actor { + wantActor, err := subject.StateGetActor(ctx, miner.ActorAddr, ts(t).Key()) + require.NoError(t, err) + return wantActor } - }) - } + } + + for _, test := range []struct { + name string + when func(t *testing.T) + request string + wantResponseStatus int + wantActor func(t *testing.T) *types.Actor + wantErr string + }{ + { + name: "no selector is error", + request: `{"jsonrpc":"2.0","method":"Filecoin.StateGetActor","params":["f01000"],"id":1}`, + wantErr: "wrong param count", + wantResponseStatus: http.StatusInternalServerError, + }, + { + name: "latest tag is ok", + request: `{"jsonrpc":"2.0","method":"Filecoin.StateGetActor","params":["f01000",{"tag":"latest"}],"id":1}`, + wantResponseStatus: http.StatusOK, + wantActor: v1StateGetActor(t, heaviest), + }, + { + name: "finalized tag when f3 disabled falls back to ec", + when: func(t *testing.T) { + mockF3.running = false + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.StateGetActor","params":["f01000",{"tag":"finalized"}],"id":1}`, + wantResponseStatus: http.StatusOK, + wantActor: v1StateGetActor(t, ecFinalized), + }, + { + 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.StateGetActor","params":["f01000",{"tag":"finalized"}],"id":1}`, + wantResponseStatus: http.StatusOK, + wantActor: v1StateGetActor(t, tipSetAtHeight(f3FinalizedEpoch)), + }, + { + 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.StateGetActor","params":["f01000",{"height":{"at":15,"anchor":{"tag":"latest"}}}],"id":1}`, + wantResponseStatus: http.StatusOK, + wantActor: v1StateGetActor(t, tipSetAtHeight(15)), + }, + } { + 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.Actor `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 { + wantActor := test.wantActor(t) + require.Equal(t, wantActor, resultOrError.Result) + } + }) + } + }) + t.Run("StateGetID", func(t *testing.T) { + for _, test := range []struct { + name string + when func(t *testing.T) + request string + wantResponseStatus int + wantID func(*testing.T) *address.Address + wantErr string + }{ + { + name: "no selector is error", + request: `{"jsonrpc":"2.0","method":"Filecoin.StateGetID","params":["f01000"],"id":1}`, + wantErr: "wrong param count", + wantResponseStatus: http.StatusInternalServerError, + }, + { + name: "latest tag is ok", + request: `{"jsonrpc":"2.0","method":"Filecoin.StateGetID","params":["f01000",{"tag":"latest"}],"id":1}`, + wantResponseStatus: http.StatusOK, + wantID: func(t *testing.T) *address.Address { + tsk := heaviest(t).Key() + wantID, err := subject.StateLookupID(ctx, miner.ActorAddr, tsk) + require.NoError(t, err) + return &wantID + }, + }, + { + name: "finalized tag when f3 disabled falls back to ec", + when: func(t *testing.T) { + mockF3.running = false + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.StateGetID","params":["f01000",{"tag":"finalized"}],"id":1}`, + wantResponseStatus: http.StatusOK, + wantID: func(t *testing.T) *address.Address { + tsk := tipSetAtHeight(f3FinalizedEpoch)(t).Key() + wantID, err := subject.StateLookupID(ctx, miner.ActorAddr, tsk) + require.NoError(t, err) + return &wantID + }, + }, + } { + 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 *address.Address `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) + } + }) + } + }) } var _ lf3.F3Backend = (*mockF3Backend)(nil) diff --git a/node/builder_chain.go b/node/builder_chain.go index 377750f9e4f..6b26ff8efe1 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -141,6 +141,7 @@ var ChainNode = Options( Override(new(full.GasModuleAPI), From(new(api.Gateway))), Override(new(full.MpoolModuleAPI), From(new(api.Gateway))), Override(new(full.StateModuleAPI), From(new(api.Gateway))), + Override(new(full.StateModuleAPIv2), From(new(full.StateModuleV2))), Override(new(stmgr.StateManagerAPI), rpcstmgr.NewRPCStateManager), Override(new(full.ActorEventAPI), From(new(api.Gateway))), Override(new(eth.EthFilecoinAPI), From(new(api.Gateway))), @@ -168,6 +169,7 @@ var ChainNode = Options( Override(new(full.GasModuleAPI), From(new(full.GasModule))), Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))), Override(new(full.StateModuleAPI), From(new(full.StateModule))), + Override(new(full.StateModuleAPIv2), From(new(full.StateModuleV2))), Override(new(stmgr.StateManagerAPI), From(new(*stmgr.StateManager))), Override(RunHelloKey, modules.RunHello), diff --git a/node/impl/full.go b/node/impl/full.go index e483be06c92..681a62b23d2 100644 --- a/node/impl/full.go +++ b/node/impl/full.go @@ -128,6 +128,7 @@ type FullNodeAPIv2 struct { fx.In full.ChainModuleAPIv2 + full.StateModuleAPIv2 } var _ v2api.FullNode = &FullNodeAPIv2{} diff --git a/node/impl/full/chain_v2.go b/node/impl/full/chain_v2.go index 583d2b5e8a3..0cd4a3b2918 100644 --- a/node/impl/full/chain_v2.go +++ b/node/impl/full/chain_v2.go @@ -16,6 +16,8 @@ import ( "github.com/filecoin-project/lotus/chain/types" ) +var _ ChainModuleAPIv2 = (*ChainModuleV2)(nil) + type ChainModuleAPIv2 interface { ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error) } @@ -27,8 +29,6 @@ type ChainModuleV2 struct { 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) diff --git a/node/impl/full/state_v2.go b/node/impl/full/state_v2.go new file mode 100644 index 00000000000..5421971a69d --- /dev/null +++ b/node/impl/full/state_v2.go @@ -0,0 +1,51 @@ +package full + +import ( + "context" + + "go.uber.org/fx" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + + "github.com/filecoin-project/lotus/chain/types" +) + +var _ StateModuleAPIv2 = (*StateModuleV2)(nil) + +type StateModuleAPIv2 interface { + StateGetActor(context.Context, address.Address, types.TipSetSelector) (*types.Actor, error) + StateGetID(context.Context, address.Address, types.TipSetSelector) (*address.Address, error) +} + +type StateModuleV2 struct { + State StateAPI + Chain ChainModuleV2 + + fx.In +} + +// TODO: Discussion: do we want to optionally strict the type of selectors that +// can be supplied here to avoid foot-guns? For example, only accept TipSetKey +// using a soft condition check here to avoid potential foot-gun but also make it +// easy to expand in the future without breaking changes if users asked for it. + +func (s *StateModuleV2) StateGetActor(ctx context.Context, addr address.Address, selector types.TipSetSelector) (*types.Actor, error) { + ts, err := s.Chain.ChainGetTipSet(ctx, selector) + if err != nil { + return nil, xerrors.Errorf("getting tipset: %w", err) + } + return s.State.StateGetActor(ctx, addr, ts.Key()) +} + +func (s *StateModuleV2) StateGetID(ctx context.Context, addr address.Address, selector types.TipSetSelector) (*address.Address, error) { + ts, err := s.Chain.ChainGetTipSet(ctx, selector) + if err != nil { + return nil, xerrors.Errorf("getting tipset: %w", err) + } + id, err := s.State.StateLookupID(ctx, addr, ts.Key()) + if err != nil { + return nil, xerrors.Errorf("looking up ID: %w", err) + } + return &id, nil +}