diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c52cbe2fe0..da9ffb78835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ ## ⭐ New Features -- feat(shed): add `lotus-shed finality-calculator` for EC finality probability computation per [FRC-0089](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0089.md) ([filecoin-project/lotus#12093](https://github.com/filecoin-project/lotus/pull/12093)) +- feat(api): integrate [FRC-0089](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0089.md) EC finality calculator into v2 API and Eth RPC, so `"finalized"` and `"safe"` tags reflect actual chain health (~20-30 epochs) rather than worst-case static 900-epoch fallback. Adds `ChainGetTipSetFinalityStatus` v2 endpoint for finality diagnostics. ([filecoin-project/lotus#13547](https://github.com/filecoin-project/lotus/pull/13547)) +- feat(shed): add `lotus-shed finality-calculator` for EC finality probability computation per FRC-0089 ([filecoin-project/lotus#12093](https://github.com/filecoin-project/lotus/pull/12093)) ## 👌 Improvements - fix(gateway): return `ErrFilterNotFound` error instead of empty result for unknown filter IDs in `EthGetFilterLogs` ([filecoin-project/lotus#13519](https://github.com/filecoin-project/lotus/pull/13519)) diff --git a/api/docgen/docgen.go b/api/docgen/docgen.go index 29aeb126690..164a3b96ba4 100644 --- a/api/docgen/docgen.go +++ b/api/docgen/docgen.go @@ -481,6 +481,13 @@ func init() { Input: ecchain, }) addExample(types.TipSetSelectors.Finalized) + addExample(&types.FinalityStatus{ + ECFinalityThresholdDepth: 30, + ECFinalizedTipSet: &ts, + F3FinalizedTipSet: &ts, + FinalizedTipSet: &ts, + Head: &ts, + }) } func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { diff --git a/api/v2api/full.go b/api/v2api/full.go index 7e5285c80d7..736879d9370 100644 --- a/api/v2api/full.go +++ b/api/v2api/full.go @@ -65,6 +65,16 @@ type FullNode interface { // ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error) //perm:read + // ChainGetTipSetFinalityStatus returns a breakdown of how the node is + // currently determining finality. The result includes the EC probabilistic + // finality depth (based on observed chain health), the F3-finalized tipset + // (if available), and the overall finalized tipset the node is using. + // + // Useful for monitoring chain health and diagnosing finality lag. + // + // Experimental: This API is experimental and may change without notice. + ChainGetTipSetFinalityStatus(context.Context) (*types.FinalityStatus, 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. diff --git a/api/v2api/gateway.go b/api/v2api/gateway.go index e1982f62e84..9458e3446de 100644 --- a/api/v2api/gateway.go +++ b/api/v2api/gateway.go @@ -18,6 +18,7 @@ var _ FullNode = (Gateway)(nil) type Gateway interface { ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error) + ChainGetTipSetFinalityStatus(context.Context) (*types.FinalityStatus, error) StateGetActor(context.Context, address.Address, types.TipSetSelector) (*types.Actor, error) StateGetID(context.Context, address.Address, types.TipSetSelector) (*address.Address, error) EthAddressToFilecoinAddress(ctx context.Context, ethAddress ethtypes.EthAddress) (address.Address, error) diff --git a/api/v2api/proxy_gen.go b/api/v2api/proxy_gen.go index fd6341fee30..e6263a9c5c0 100644 --- a/api/v2api/proxy_gen.go +++ b/api/v2api/proxy_gen.go @@ -26,6 +26,8 @@ type FullNodeStruct struct { type FullNodeMethods struct { ChainGetTipSet func(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) `perm:"read"` + ChainGetTipSetFinalityStatus func(p0 context.Context) (*types.FinalityStatus, error) `perm:"read"` + EthAccounts func(p0 context.Context) ([]ethtypes.EthAddress, error) `perm:"read"` EthAddressToFilecoinAddress func(p0 context.Context, p1 ethtypes.EthAddress) (address.Address, error) `perm:"read"` @@ -137,6 +139,8 @@ type GatewayStruct struct { type GatewayMethods struct { ChainGetTipSet func(p0 context.Context, p1 types.TipSetSelector) (*types.TipSet, error) `` + ChainGetTipSetFinalityStatus func(p0 context.Context) (*types.FinalityStatus, error) `` + Discover func(p0 context.Context) (apitypes.OpenRPCDocument, error) `` EthAccounts func(p0 context.Context) ([]ethtypes.EthAddress, error) `` @@ -254,6 +258,17 @@ func (s *FullNodeStub) ChainGetTipSet(p0 context.Context, p1 types.TipSetSelecto return nil, ErrNotSupported } +func (s *FullNodeStruct) ChainGetTipSetFinalityStatus(p0 context.Context) (*types.FinalityStatus, error) { + if s.Internal.ChainGetTipSetFinalityStatus == nil { + return nil, ErrNotSupported + } + return s.Internal.ChainGetTipSetFinalityStatus(p0) +} + +func (s *FullNodeStub) ChainGetTipSetFinalityStatus(p0 context.Context) (*types.FinalityStatus, error) { + return nil, ErrNotSupported +} + func (s *FullNodeStruct) EthAccounts(p0 context.Context) ([]ethtypes.EthAddress, error) { if s.Internal.EthAccounts == nil { return *new([]ethtypes.EthAddress), ErrNotSupported @@ -815,6 +830,17 @@ func (s *GatewayStub) ChainGetTipSet(p0 context.Context, p1 types.TipSetSelector return nil, ErrNotSupported } +func (s *GatewayStruct) ChainGetTipSetFinalityStatus(p0 context.Context) (*types.FinalityStatus, error) { + if s.Internal.ChainGetTipSetFinalityStatus == nil { + return nil, ErrNotSupported + } + return s.Internal.ChainGetTipSetFinalityStatus(p0) +} + +func (s *GatewayStub) ChainGetTipSetFinalityStatus(p0 context.Context) (*types.FinalityStatus, error) { + return nil, ErrNotSupported +} + func (s *GatewayStruct) Discover(p0 context.Context) (apitypes.OpenRPCDocument, error) { if s.Internal.Discover == nil { return *new(apitypes.OpenRPCDocument), ErrNotSupported diff --git a/api/v2api/v2mocks/mock_full.go b/api/v2api/v2mocks/mock_full.go index 7b25dbd1731..4ddd36e84ab 100644 --- a/api/v2api/v2mocks/mock_full.go +++ b/api/v2api/v2mocks/mock_full.go @@ -57,6 +57,21 @@ func (mr *MockFullNodeMockRecorder) ChainGetTipSet(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainGetTipSet", reflect.TypeOf((*MockFullNode)(nil).ChainGetTipSet), arg0, arg1) } +// ChainGetTipSetFinalityStatus mocks base method. +func (m *MockFullNode) ChainGetTipSetFinalityStatus(arg0 context.Context) (*types.FinalityStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChainGetTipSetFinalityStatus", arg0) + ret0, _ := ret[0].(*types.FinalityStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChainGetTipSetFinalityStatus indicates an expected call of ChainGetTipSetFinalityStatus. +func (mr *MockFullNodeMockRecorder) ChainGetTipSetFinalityStatus(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainGetTipSetFinalityStatus", reflect.TypeOf((*MockFullNode)(nil).ChainGetTipSetFinalityStatus), arg0) +} + // EthAccounts mocks base method. func (m *MockFullNode) EthAccounts(arg0 context.Context) ([]ethtypes.EthAddress, error) { m.ctrl.T.Helper() diff --git a/build/openrpc/v2/full.json b/build/openrpc/v2/full.json index 8b824d0dfcd..0e75990d4db 100644 --- a/build/openrpc/v2/full.json +++ b/build/openrpc/v2/full.json @@ -152,7 +152,335 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L246" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L250" + } + }, + { + "name": "Filecoin.ChainGetTipSetFinalityStatus", + "description": "```go\nfunc (s *FullNodeStruct) ChainGetTipSetFinalityStatus(p0 context.Context) (*types.FinalityStatus, error) {\n\tif s.Internal.ChainGetTipSetFinalityStatus == nil {\n\t\treturn nil, ErrNotSupported\n\t}\n\treturn s.Internal.ChainGetTipSetFinalityStatus(p0)\n}\n```", + "summary": "ChainGetTipSetFinalityStatus returns a breakdown of how the node is\ncurrently determining finality. The result includes the EC probabilistic\nfinality depth (based on observed chain health), the F3-finalized tipset\n(if available), and the overall finalized tipset the node is using.\n\nUseful for monitoring chain health and diagnosing finality lag.\n\nExperimental: This API is experimental and may change without notice.\n", + "paramStructure": "by-position", + "params": [], + "result": { + "name": "*types.FinalityStatus", + "description": "*types.FinalityStatus", + "summary": "", + "schema": { + "examples": [ + { + "ecFinalityThresholdDepth": 30, + "ecFinalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "f3FinalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "finalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "head": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + } + } + ], + "additionalProperties": false, + "properties": { + "ecFinalityThresholdDepth": { + "title": "number", + "type": "number" + }, + "ecFinalizedTipSet": { + "additionalProperties": false, + "type": "object" + }, + "f3FinalizedTipSet": { + "additionalProperties": false, + "type": "object" + }, + "finalizedTipSet": { + "additionalProperties": false, + "type": "object" + }, + "head": { + "additionalProperties": false, + "type": "object" + } + }, + "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#L261" } }, { @@ -199,7 +527,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L257" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L272" } }, { @@ -254,7 +582,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L268" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L283" } }, { @@ -283,7 +611,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L279" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L294" } }, { @@ -420,7 +748,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L290" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L305" } }, { @@ -449,7 +777,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L301" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L316" } }, { @@ -503,7 +831,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L312" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L327" } }, { @@ -594,7 +922,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L323" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L338" } }, { @@ -622,7 +950,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L334" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L349" } }, { @@ -712,7 +1040,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L345" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L360" } }, { @@ -968,7 +1296,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L356" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L371" } }, { @@ -1213,7 +1541,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L367" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L382" } }, { @@ -1489,7 +1817,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L378" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L393" } }, { @@ -1782,7 +2110,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L389" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L404" } }, { @@ -1838,7 +2166,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L400" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L415" } }, { @@ -1883,7 +2211,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L411" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L426" } }, { @@ -1981,7 +2309,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L422" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L437" } }, { @@ -2047,7 +2375,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L433" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L448" } }, { @@ -2113,7 +2441,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L444" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L459" } }, { @@ -2222,7 +2550,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L455" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L470" } }, { @@ -2280,7 +2608,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L466" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L481" } }, { @@ -2402,7 +2730,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L477" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L492" } }, { @@ -2611,7 +2939,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L488" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L503" } }, { @@ -2809,7 +3137,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L499" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L514" } }, { @@ -3001,7 +3329,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L510" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L525" } }, { @@ -3210,7 +3538,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L521" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L536" } }, { @@ -3301,7 +3629,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L532" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L547" } }, { @@ -3359,7 +3687,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L543" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L558" } }, { @@ -3617,7 +3945,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L554" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L569" } }, { @@ -3892,7 +4220,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L565" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L580" } }, { @@ -3920,7 +4248,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L576" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L591" } }, { @@ -3958,7 +4286,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L587" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L602" } }, { @@ -4066,7 +4394,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L598" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L613" } }, { @@ -4104,7 +4432,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L609" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L624" } }, { @@ -4133,7 +4461,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L620" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L635" } }, { @@ -4196,7 +4524,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L631" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L646" } }, { @@ -4259,7 +4587,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L642" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L657" } }, { @@ -4322,7 +4650,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L653" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L668" } }, { @@ -4367,7 +4695,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L664" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L679" } }, { @@ -4489,7 +4817,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L675" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L690" } }, { @@ -4665,7 +4993,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L686" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L701" } }, { @@ -4820,7 +5148,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L697" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L712" } }, { @@ -4942,7 +5270,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L708" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L723" } }, { @@ -4996,7 +5324,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L719" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L734" } }, { @@ -5050,7 +5378,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L730" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L745" } }, { @@ -5113,7 +5441,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L741" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L756" } }, { @@ -5140,7 +5468,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L752" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L767" } }, { @@ -5167,7 +5495,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L763" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L778" } }, { @@ -5297,7 +5625,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L774" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L789" } }, { @@ -5395,7 +5723,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L785" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L800" } }, { @@ -5422,7 +5750,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L796" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L811" } } ] diff --git a/build/openrpc/v2/gateway.json b/build/openrpc/v2/gateway.json index 5301b268ca8..d46dc94f7d0 100644 --- a/build/openrpc/v2/gateway.json +++ b/build/openrpc/v2/gateway.json @@ -152,7 +152,335 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L807" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L822" + } + }, + { + "name": "Filecoin.ChainGetTipSetFinalityStatus", + "description": "```go\nfunc (s *GatewayStruct) ChainGetTipSetFinalityStatus(p0 context.Context) (*types.FinalityStatus, error) {\n\tif s.Internal.ChainGetTipSetFinalityStatus == nil {\n\t\treturn nil, ErrNotSupported\n\t}\n\treturn s.Internal.ChainGetTipSetFinalityStatus(p0)\n}\n```", + "summary": "There are not yet any comments for this method.", + "paramStructure": "by-position", + "params": [], + "result": { + "name": "*types.FinalityStatus", + "description": "*types.FinalityStatus", + "summary": "", + "schema": { + "examples": [ + { + "ecFinalityThresholdDepth": 30, + "ecFinalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "f3FinalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "finalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "head": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + } + } + ], + "additionalProperties": false, + "properties": { + "ecFinalityThresholdDepth": { + "title": "number", + "type": "number" + }, + "ecFinalizedTipSet": { + "additionalProperties": false, + "type": "object" + }, + "f3FinalizedTipSet": { + "additionalProperties": false, + "type": "object" + }, + "finalizedTipSet": { + "additionalProperties": false, + "type": "object" + }, + "head": { + "additionalProperties": false, + "type": "object" + } + }, + "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#L833" } }, { @@ -192,7 +520,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L818" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L844" } }, { @@ -239,7 +567,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L829" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L855" } }, { @@ -294,7 +622,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L840" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L866" } }, { @@ -323,7 +651,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L851" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L877" } }, { @@ -460,7 +788,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L862" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L888" } }, { @@ -489,7 +817,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L873" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L899" } }, { @@ -543,7 +871,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L884" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L910" } }, { @@ -634,7 +962,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L895" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L921" } }, { @@ -662,7 +990,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L906" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L932" } }, { @@ -752,7 +1080,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L917" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L943" } }, { @@ -1008,7 +1336,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L928" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L954" } }, { @@ -1253,7 +1581,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L939" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L965" } }, { @@ -1529,7 +1857,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L950" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L976" } }, { @@ -1822,7 +2150,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L961" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L987" } }, { @@ -1878,7 +2206,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L972" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L998" } }, { @@ -1923,7 +2251,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L983" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1009" } }, { @@ -2021,7 +2349,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L994" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1020" } }, { @@ -2087,7 +2415,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1005" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1031" } }, { @@ -2153,7 +2481,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1016" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1042" } }, { @@ -2262,7 +2590,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1027" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1053" } }, { @@ -2320,7 +2648,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1038" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1064" } }, { @@ -2442,7 +2770,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1049" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1075" } }, { @@ -2651,7 +2979,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1060" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1086" } }, { @@ -2849,7 +3177,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1071" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1097" } }, { @@ -3041,7 +3369,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1082" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1108" } }, { @@ -3250,7 +3578,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1093" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1119" } }, { @@ -3341,7 +3669,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1104" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1130" } }, { @@ -3399,7 +3727,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1115" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1141" } }, { @@ -3657,7 +3985,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1126" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1152" } }, { @@ -3932,7 +4260,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1137" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1163" } }, { @@ -3960,7 +4288,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1148" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1174" } }, { @@ -3998,7 +4326,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1159" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1185" } }, { @@ -4106,7 +4434,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1170" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1196" } }, { @@ -4144,7 +4472,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1181" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1207" } }, { @@ -4173,7 +4501,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1192" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1218" } }, { @@ -4236,7 +4564,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1203" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1229" } }, { @@ -4299,7 +4627,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1214" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1240" } }, { @@ -4362,7 +4690,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1225" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1251" } }, { @@ -4407,7 +4735,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1236" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1262" } }, { @@ -4529,7 +4857,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1247" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1273" } }, { @@ -4705,7 +5033,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1258" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1284" } }, { @@ -4860,7 +5188,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1269" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1295" } }, { @@ -4982,7 +5310,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1280" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1306" } }, { @@ -5036,7 +5364,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1291" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1317" } }, { @@ -5090,7 +5418,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1302" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1328" } }, { @@ -5153,7 +5481,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1313" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1339" } }, { @@ -5180,7 +5508,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1324" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1350" } }, { @@ -5207,7 +5535,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1335" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1361" } }, { @@ -5337,7 +5665,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1346" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1372" } }, { @@ -5435,7 +5763,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1357" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1383" } }, { @@ -5462,7 +5790,7 @@ "deprecated": false, "externalDocs": { "description": "Github remote link", - "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1368" + "url": "https://github.com/filecoin-project/lotus/blob/master/api/v2api/proxy_gen.go#L1394" } } ] diff --git a/chain/ecfinality/cache.go b/chain/ecfinality/cache.go new file mode 100644 index 00000000000..30a683c9b15 --- /dev/null +++ b/chain/ecfinality/cache.go @@ -0,0 +1,138 @@ +package ecfinality + +import ( + "context" + "errors" + "math" + "sync" + + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" +) + +// ECFinalityCalculator is the interface for EC finality queries, implemented +// by ECFinalityCache for production and by mocks for testing. +type ECFinalityCalculator interface { + GetStatus(ctx context.Context) (*ECFinalityStatus, error) + GetFinalizedTipSet(ctx context.Context) (*types.TipSet, error) +} + +// ECFinalityStatus holds the resolved EC finality state for a given head. +type ECFinalityStatus struct { + // ThresholdDepth is the shallowest epoch depth at which the reorg + // probability drops below 2^-30. -1 if the threshold is not met. + ThresholdDepth int + // FinalizedTipSet is the tipset at that depth, or nil if not met. + FinalizedTipSet *types.TipSet + // Head is the chain head the computation was performed against. + Head *types.TipSet +} + +// ECFinalityCache caches the EC finality threshold depth, recomputing only +// when the chain head changes. The cost is dominated by the Skellam PMF +// calculation in FindThresholdDepth, not by tipset loading (which hits the +// ChainStore's ARC cache). The cache is safe for concurrent use. +type ECFinalityCache struct { + cs *store.ChainStore + windowSize int // finality + 5 (the lookback the calculator needs) + + mu sync.Mutex + cached *ECFinalityStatus +} + +// NewECFinalityCache creates a new cache backed by the given ChainStore. +// The finality parameter is the lookback depth for the L distribution +// (typically policy.ChainFinality = 900). +func NewECFinalityCache(cs *store.ChainStore, finality int) *ECFinalityCache { + return &ECFinalityCache{ + cs: cs, + windowSize: finality + 5, + } +} + +// GetStatus returns the full EC finality state for the current head: +// threshold depth, finalized tipset, and the head used. The result is +// cached and recomputed only when the chain head changes. +// +// Errors are not cached; a transient failure allows retry on the next call. +// The computation uses a cancellation-resistant context so that a single +// caller's context cancellation does not poison the cache for subsequent +// callers. +func (c *ECFinalityCache) GetStatus(ctx context.Context) (*ECFinalityStatus, error) { + head := c.cs.GetHeaviestTipSet() + if head == nil { + return nil, errors.New("no known heaviest tipset") + } + + c.mu.Lock() + defer c.mu.Unlock() + + // Return cached result if head hasn't changed. + if c.cached != nil && c.cached.Head.Key() == head.Key() { + return c.cached, nil + } + + // Detach from the caller's context so that cancellation of one request + // does not prevent the result from being cached for others. + computeCtx := context.WithoutCancel(ctx) + + chain, err := c.walkChain(computeCtx, head) + if err != nil { + return nil, err + } + + guarantee := math.Pow(2, float64(DefaultSafetyExponent)) + threshold := FindThresholdDepth(chain, c.windowSize-5, DefaultBlocksPerEpoch, DefaultByzantineFraction, guarantee) + + status := &ECFinalityStatus{ + ThresholdDepth: threshold, + Head: head, + } + + if threshold >= 0 { + height := max(0, head.Height()-abi.ChainEpoch(threshold)) + ts, err := c.cs.GetTipsetByHeight(computeCtx, height, head, true) + if err != nil { + return nil, err + } + status.FinalizedTipSet = ts + } + + c.cached = status + return status, nil +} + +// GetFinalizedTipSet returns the most recent tipset where the probability of +// reorg drops below 2^-30. Returns nil if the threshold is not met (chain may +// be degraded). This is a convenience wrapper; use GetStatus for the full +// finality breakdown. +func (c *ECFinalityCache) GetFinalizedTipSet(ctx context.Context) (*types.TipSet, error) { + s, err := c.GetStatus(ctx) + if err != nil { + return nil, err + } + return s.FinalizedTipSet, nil +} + +// walkChain walks back from head collecting block counts for the calculator. +// Each LoadTipSet call typically hits the ChainStore's ARC cache. +func (c *ECFinalityCache) walkChain(ctx context.Context, head *types.TipSet) ([]int, error) { + needed := c.windowSize + chain := make([]int, 0, needed) + ts := head + for len(chain) < needed { + chain = append(chain, len(ts.Cids())) + parent, err := c.cs.LoadTipSet(ctx, ts.Parents()) + if err != nil { + return nil, err + } + ts = parent + } + // Reverse to chronological order (oldest first). + for i, j := 0, len(chain)-1; i < j; i, j = i+1, j-1 { + chain[i], chain[j] = chain[j], chain[i] + } + return chain, nil +} diff --git a/chain/ecfinality/calculator.go b/chain/ecfinality/calculator.go new file mode 100644 index 00000000000..50b07fefcec --- /dev/null +++ b/chain/ecfinality/calculator.go @@ -0,0 +1,244 @@ +// Package ecfinality implements the FRC-0089 EC finality calculator. +// +// The calculator computes an upper bound on the probability that a confirmed +// tipset could be reorganized out of the canonical chain by an adversarial +// fork, using observed chain data (block counts per epoch). Under healthy +// network conditions (~5 blocks/epoch), the 2^-30 finality guarantee +// (roughly one-in-a-billion chance of reorg) is typically achieved within +// ~30 epochs (~15 minutes), compared to the static 900-epoch (~7.5 hour) +// EC finality assumption which is based on worst-case network conditions. +// +// Reference: https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0089.md +// Python reference: https://github.com/consensus-shipyard/ec-finality-calculator +package ecfinality + +import ( + "math" + + skellampmf "github.com/rvagg/go-skellam-pmf" +) + +const ( + // BisectLow and BisectHigh define the search range for the bisect algorithm + // that finds the epoch depth at which the finality guarantee is met. A low + // bound of 3 avoids evaluating trivially shallow depths; a high bound of + // 200 accommodates degraded chains that take longer to finalize. + BisectLow = 3 + BisectHigh = 200 + + // DefaultBlocksPerEpoch is the Filecoin mainnet expected block production rate. + DefaultBlocksPerEpoch = 5.0 + + // DefaultByzantineFraction is the standard Filecoin security assumption for + // adversarial mining power. + DefaultByzantineFraction = 0.3 + + // DefaultSafetyExponent is the target reorg probability as a power of 2. + // 2^-30 (~one-in-a-billion) is the standard Filecoin finality guarantee. + DefaultSafetyExponent = -30 +) + +// CalcValidatorProb computes the upper-bound probability that a confirmed +// tipset could be reorganized out of the canonical chain. This is a Go port +// of the Python reference implementation from FRC-0089 +// (finality_calc_validator.py). +// +// Parameters: +// - chain: block counts per epoch (index 0 = earliest epoch) +// - finality: lookback depth for the L distribution (900 on mainnet) +// - blocksPerEpoch: expected blocks per epoch (5 for mainnet) +// - byzantineFraction: upper bound on adversarial power fraction (e.g. 0.3) +// - currentEpoch: index into chain for the current epoch +// - targetEpoch: index into chain for the epoch being evaluated +func CalcValidatorProb(chain []int, finality int, blocksPerEpoch float64, byzantineFraction float64, currentEpoch int, targetEpoch int) float64 { + if currentEpoch <= targetEpoch || targetEpoch < 0 || currentEpoch >= len(chain) { + return 1.0 + } + + const negligibleThreshold = 1e-25 + + maxKL := 400 + maxKB := (currentEpoch - targetEpoch) * int(blocksPerEpoch) + maxKM := 400 + maxIM := 100 + + rateMaliciousBlocks := blocksPerEpoch * byzantineFraction + rateHonestBlocks := blocksPerEpoch - rateMaliciousBlocks + + // Compute L: adversarial lead distribution at target epoch + prL := make([]float64, maxKL+1) + + for k := 0; k <= maxKL; k++ { + sumExpectedAdversarialBlocksI := 0.0 + sumChainBlocksI := 0 + + for i := targetEpoch; i > max(0, currentEpoch-finality); i-- { + sumExpectedAdversarialBlocksI += rateMaliciousBlocks + sumChainBlocksI += chain[i-1] + prLi := poissonProb(sumExpectedAdversarialBlocksI, float64(k+sumChainBlocksI)) + prL[k] = max(prL[k], prLi) + } + if k > 1 && prL[k] < negligibleThreshold && prL[k] < prL[k-1] { + maxKL = k + prL = prL[:k+1] + break + } + } + + prL[0] += 1 - sumFloat64(prL) + + // Compute B: adversarial blocks during settlement period + prB := make([]float64, maxKB+1) + + for k := 0; k <= maxKB; k++ { + prB[k] = poissonProb(float64(currentEpoch-targetEpoch)*rateMaliciousBlocks, float64(k)) + + if k > 1 && prB[k] < negligibleThreshold && prB[k] < prB[k-1] { + maxKB = k + prB = prB[:k+1] + break + } + } + + // Compute M: adversarial mining advantage in the future (Skellam distribution) + prHgt0 := 1 - poissonProb(rateHonestBlocks, 0) + + expZ := 0.0 + for k := 0; k < int(4*blocksPerEpoch); k++ { + pmf := poissonProb(rateMaliciousBlocks, float64(k)) + expZ += ((rateHonestBlocks + float64(k)) / math.Pow(2, float64(k))) * pmf + } + + ratePublicChain := prHgt0 * expZ + + prM := make([]float64, maxKM+1) + for k := 0; k <= maxKM; k++ { + for i := maxIM; i > 0; i-- { + probMI := skellampmf.SkellamPMF(k, float64(i)*rateMaliciousBlocks, float64(i)*ratePublicChain) + + if probMI < negligibleThreshold && probMI < prM[k] { + break + } + prM[k] = max(prM[k], probMI) + } + + if k > 1 && prM[k] < negligibleThreshold && prM[k] < prM[k-1] { + maxKM = k + prM = prM[:k+1] + break + } + } + + prM[0] += 1 - sumFloat64(prM) + + // Compute reorg probability upper bound via convolution + cumsumL := cumsum(prL) + cumsumB := cumsum(prB) + cumsumM := cumsum(prM) + + k := sumInt(chain[targetEpoch:currentEpoch]) + + sumLgeK := cumsumL[len(cumsumL)-1] + if k > 0 { + sumLgeK -= cumsumL[min(k-1, maxKL)] + } + + doubleSum := 0.0 + + for l := range k { + sumBgeKminL := cumsumB[len(cumsumB)-1] + if k-l-1 > 0 { + sumBgeKminL -= cumsumB[min(k-l-1, maxKB)] + } + doubleSum += prL[min(l, maxKL)] * sumBgeKminL + + for b := 0; b < k-l; b++ { + sumMgeKminLminB := cumsumM[len(cumsumM)-1] + if k-l-b-1 > 0 { + sumMgeKminLminB -= cumsumM[min(k-l-b-1, maxKM)] + } + doubleSum += prL[min(l, maxKL)] * prB[min(b, maxKB)] * sumMgeKminLminB + } + } + + prError := sumLgeK + doubleSum + + return min(prError, 1.0) +} + +// FindThresholdDepth performs a bisect search to find the shallowest depth at +// which the reorg probability drops below the given guarantee. Returns -1 if +// the guarantee is not met within the search range. +func FindThresholdDepth(chain []int, finality int, blocksPerEpoch float64, byzantineFraction float64, guarantee float64) int { + currentEpoch := len(chain) - 1 + low, high := BisectLow, min(BisectHigh, currentEpoch) + + if low >= high { + return -1 + } + + probLow := CalcValidatorProb(chain, finality, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-low) + if probLow < guarantee { + return low + } + + probHigh := CalcValidatorProb(chain, finality, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-high) + if probHigh > guarantee { + return -1 + } + + for low < high { + mid := (low + high) / 2 + prob := CalcValidatorProb(chain, finality, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-mid) + if prob < guarantee { + high = mid + } else { + low = mid + 1 + } + } + return low +} + +func poissonProb(lambda float64, x float64) float64 { + return math.Exp(poissonLogProb(lambda, x)) +} + +func poissonLogProb(lambda float64, x float64) float64 { + if x < 0 || math.Floor(x) != x { + return math.Inf(-1) + } + if lambda == 0 { + if x == 0 { + return 0 // P(X=0 | lambda=0) = 1, log(1) = 0 + } + return math.Inf(-1) + } + lg, _ := math.Lgamma(math.Floor(x) + 1) + return x*math.Log(lambda) - lambda - lg +} + +func sumFloat64(s []float64) float64 { + var total float64 + for _, v := range s { + total += v + } + return total +} + +func sumInt(s []int) int { + var total int + for _, v := range s { + total += v + } + return total +} + +func cumsum(arr []float64) []float64 { + result := make([]float64, len(arr)) + var s float64 + for i, v := range arr { + s += v + result[i] = s + } + return result +} diff --git a/cmd/lotus-shed/finality_test.go b/chain/ecfinality/calculator_test.go similarity index 69% rename from cmd/lotus-shed/finality_test.go rename to chain/ecfinality/calculator_test.go index c2532535653..880eda520df 100644 --- a/cmd/lotus-shed/finality_test.go +++ b/chain/ecfinality/calculator_test.go @@ -1,4 +1,4 @@ -package main +package ecfinality import ( "math" @@ -7,6 +7,9 @@ import ( "github.com/stretchr/testify/require" ) +// The finality parameter used by the Python reference (and Filecoin mainnet). +const testFinality = 900 + // Test vectors generated by the Python reference implementation from FRC-0089: // https://github.com/consensus-shipyard/ec-finality-calculator (finality_calc_validator.py) // @@ -63,7 +66,7 @@ var pythonReferenceChain = []int{ 5, 10, 2, 4, 3, } -// pythonReferenceResults maps depth -> error probability from the Python reference. +// pythonReferenceResults maps depth -> reorg probability from the Python reference. var pythonReferenceResults = map[int]float64{ 5: 1.58182730260265891863e-03, 10: 1.67515743138728720072e-04, @@ -77,14 +80,14 @@ var pythonReferenceResults = map[int]float64{ 100: 3.21616912956779552478e-24, } -func TestFinalityCalcValidator_PythonReference(t *testing.T) { +func TestCalcValidatorProb_PythonReference(t *testing.T) { req := require.New(t) chain := pythonReferenceChain currentEpoch := len(chain) - 1 for depth, wantProb := range pythonReferenceResults { targetEpoch := currentEpoch - depth - gotProb := FinalityCalcValidator(chain, 5.0, 0.3, currentEpoch, targetEpoch) + gotProb := CalcValidatorProb(chain, testFinality, 5.0, 0.3, currentEpoch, targetEpoch) relErr := math.Abs(gotProb-wantProb) / math.Abs(wantProb) req.LessOrEqualf(relErr, 1e-12, @@ -92,7 +95,7 @@ func TestFinalityCalcValidator_PythonReference(t *testing.T) { } } -func TestFinalityCalcValidator_HealthyChain(t *testing.T) { +func TestCalcValidatorProb_HealthyChain(t *testing.T) { req := require.New(t) // A perfectly healthy chain with 5 blocks per epoch should achieve @@ -104,14 +107,14 @@ func TestFinalityCalcValidator_HealthyChain(t *testing.T) { currentEpoch := len(chain) - 1 guarantee := math.Pow(2, -30) - prob30 := FinalityCalcValidator(chain, 5.0, 0.3, currentEpoch, currentEpoch-30) + prob30 := CalcValidatorProb(chain, testFinality, 5.0, 0.3, currentEpoch, currentEpoch-30) req.Less(prob30, guarantee, "healthy chain at depth 30 should be below 2^-30") - prob5 := FinalityCalcValidator(chain, 5.0, 0.3, currentEpoch, currentEpoch-5) - req.Greater(prob5, prob30, "shallower depth should have higher error probability") + prob5 := CalcValidatorProb(chain, testFinality, 5.0, 0.3, currentEpoch, currentEpoch-5) + req.Greater(prob5, prob30, "shallower depth should have higher reorg probability") } -func TestFinalityCalcValidator_DegradedChain(t *testing.T) { +func TestCalcValidatorProb_DegradedChain(t *testing.T) { req := require.New(t) // A degraded chain with only 2 blocks per epoch should have much worse @@ -123,6 +126,51 @@ func TestFinalityCalcValidator_DegradedChain(t *testing.T) { currentEpoch := len(chain) - 1 guarantee := math.Pow(2, -30) - prob30 := FinalityCalcValidator(chain, 5.0, 0.3, currentEpoch, currentEpoch-30) + prob30 := CalcValidatorProb(chain, testFinality, 5.0, 0.3, currentEpoch, currentEpoch-30) req.GreaterOrEqual(prob30, guarantee, "degraded chain at depth 30 should NOT achieve 2^-30") } + +func TestFindThresholdDepth_HealthyChain(t *testing.T) { + req := require.New(t) + + chain := make([]int, 905) + for i := range chain { + chain[i] = 5 + } + guarantee := math.Pow(2, -30) + + depth := FindThresholdDepth(chain, testFinality, 5.0, 0.3, guarantee) + req.Greater(depth, 0, "healthy chain should find a threshold") + req.Less(depth, 35, "healthy chain should finalize well before depth 35") +} + +func TestFindThresholdDepth_DegradedChain(t *testing.T) { + req := require.New(t) + + // All-2s chain is too degraded to achieve 2^-30 within the bisect + // search range (BisectHigh=200), so threshold is not found + chain := make([]int, 905) + for i := range chain { + chain[i] = 2 + } + guarantee := math.Pow(2, -30) + + depth := FindThresholdDepth(chain, testFinality, 5.0, 0.3, guarantee) + req.Equal(-1, depth, "severely degraded chain should not find threshold within search range") +} + +func TestFindThresholdDepth_MildlyDegradedChain(t *testing.T) { + req := require.New(t) + + // All-3s chain is degraded but should still find a threshold, + // just deeper than a healthy chain + chain := make([]int, 905) + for i := range chain { + chain[i] = 3 + } + guarantee := math.Pow(2, -30) + + depth := FindThresholdDepth(chain, testFinality, 5.0, 0.3, guarantee) + req.Greater(depth, 35, "mildly degraded chain should require more depth than healthy") + req.Less(depth, BisectHigh, "mildly degraded chain should still find a threshold") +} diff --git a/chain/types/finality.go b/chain/types/finality.go new file mode 100644 index 00000000000..9bfacae5583 --- /dev/null +++ b/chain/types/finality.go @@ -0,0 +1,28 @@ +package types + +// FinalityStatus describes how the node is currently determining finality, +// combining probabilistic EC finality (based on observed chain health) with +// F3 fast finality when available. +type FinalityStatus struct { + // ECFinalityThresholdDepth is the shallowest epoch depth at which the + // probability of a chain reorganization drops below 2^-30 (~one in a + // billion). A value of -1 indicates the threshold was not met within the + // search range, which suggests degraded chain health. + ECFinalityThresholdDepth int `json:"ecFinalityThresholdDepth"` + + // ECFinalizedTipSet is the most recent tipset where the reorg probability + // is below 2^-30, based on observed block production. Nil if the + // threshold is not met. + ECFinalizedTipSet *TipSet `json:"ecFinalizedTipSet"` + + // F3FinalizedTipSet is the tipset finalized by F3 (Fast Finality), if F3 + // is running and has issued a certificate. Nil if F3 is not available. + F3FinalizedTipSet *TipSet `json:"f3FinalizedTipSet"` + + // FinalizedTipSet is the overall finalized tipset used by the node, + // taking the most recent of F3 and EC calculator results. + FinalizedTipSet *TipSet `json:"finalizedTipSet"` + + // Head is the current chain head used for the computation. + Head *TipSet `json:"head"` +} diff --git a/cmd/lotus-shed/finality.go b/cmd/lotus-shed/finality.go index cbe2b8c41af..f97504ddb8c 100644 --- a/cmd/lotus-shed/finality.go +++ b/cmd/lotus-shed/finality.go @@ -8,11 +8,11 @@ import ( "slices" "strconv" - skellampmf "github.com/rvagg/go-skellam-pmf" "github.com/urfave/cli/v2" "github.com/filecoin-project/lotus/build/buildconstants" "github.com/filecoin-project/lotus/chain/actors/policy" + "github.com/filecoin-project/lotus/chain/ecfinality" lcli "github.com/filecoin-project/lotus/cli" "github.com/filecoin-project/lotus/lib/tablewriter" ) @@ -21,24 +21,18 @@ const ( // recentHealthWindow is the number of recent epochs used to compute average // blocks/epoch for the chain health display. recentHealthWindow = 30 - - // bisectLow and bisectHigh define the search range for the bisect algorithm - // that finds the epoch depth at which the finality guarantee is met. A low - // bound of 3 avoids evaluating trivially shallow depths; a high bound of - // 200 accommodates degraded chains that take longer to finalize. - bisectLow = 3 - bisectHigh = 200 ) var finalityCmd = &cli.Command{ Name: "finality-calculator", - Usage: "Calculate the EC finality probability of a tipset", - Description: `Compute the probability that a previous blockchain tipset gets replaced, -based on FRC-0089 (https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0089.md). + Usage: "Calculate the reorg probability of a tipset at various depths", + Description: `Compute the probability that a confirmed tipset could be reorganized out +of the canonical chain, based on observed block production and the FRC-0089 model +(https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0089.md). -Under healthy network conditions (tipsets with ~5 blocks), the 2^-30 error guarantee -is typically achieved within ~30 epochs (~15 minutes), compared to the static 900-epoch -(~7.5 hour) EC finality assumption. +Under healthy network conditions (tipsets with ~5 blocks), the 2^-30 finality guarantee +(~one-in-a-billion chance of reorg) is typically achieved within ~30 epochs (~15 minutes), +compared to the static 900-epoch (~7.5 hour) EC finality assumption. Chain history can be read from a running Lotus node or from a text file where each line contains the number of blocks in an epoch (most recent epoch last). @@ -52,22 +46,22 @@ machine-readable output of all 900 epochs.`, }, &cli.Float64Flag{ Name: "blocks-per-epoch", - Value: 5.0, + Value: ecfinality.DefaultBlocksPerEpoch, Usage: "Expected number of blocks per epoch (Filecoin mainnet protocol constant is 5). Changing this models a different expected block production rate.", }, &cli.Float64Flag{ Name: "byzantine-fraction", - Value: 0.3, + Value: ecfinality.DefaultByzantineFraction, Usage: "Assumed upper bound on adversarial mining power as a fraction (0.0-1.0). The standard Filecoin security assumption is 0.3 (30%). Lower values model a more secure network; higher values model stronger adversaries.", }, &cli.IntFlag{ Name: "safety-exponent", - Value: -30, - Usage: "Safety target as a power of 2 (e.g. -30 means 2^-30). This is the maximum acceptable probability of a tipset being replaced. The Filecoin standard is -30, which the 900-epoch static finality was designed to achieve.", + Value: ecfinality.DefaultSafetyExponent, + Usage: "Safety target as a power of 2 (e.g. -30 means 2^-30). This is the maximum acceptable probability of a tipset being reorganized. The Filecoin standard is -30, which the 900-epoch static finality was designed to achieve.", }, &cli.BoolFlag{ Name: "csv", - Usage: "Output raw CSV (epoch,depth,error_probability) for all epochs back to EC finality, suitable for piping to other tools or plotting.", + Usage: "Output raw CSV (epoch,depth,reorg_probability) for all epochs back to EC finality, suitable for piping to other tools or plotting.", }, }, Action: func(cctx *cli.Context) error { @@ -89,7 +83,7 @@ machine-readable output of all 900 epochs.`, headEpoch = int(head.Height()) readLength := int(policy.ChainFinality) + 5 chain = append(chain, len(head.Cids())) - for range readLength { + for range readLength - 1 { head, err = api.ChainGetTipSet(ctx, head.Parents()) if err != nil { return err @@ -129,6 +123,7 @@ machine-readable output of all 900 epochs.`, byzantineFraction := cctx.Float64("byzantine-fraction") safetyExponent := cctx.Int("safety-exponent") guarantee := math.Pow(2, float64(safetyExponent)) + finality := int(policy.ChainFinality) epochDurationSecs := float64(buildconstants.BlockDelaySecs) currentEpoch := len(chain) - 1 // head of the chain segment we are considering, not actual Filecoin epoch number out := cctx.App.Writer @@ -143,9 +138,9 @@ machine-readable output of all 900 epochs.`, // CSV mode: raw output for all epochs if cctx.Bool("csv") { - _, _ = fmt.Fprintln(out, "epoch,depth,error_probability") - for i := range min(int(policy.ChainFinality), currentEpoch) { - prob := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-i) + _, _ = fmt.Fprintln(out, "epoch,depth,reorg_probability") + for i := range min(finality, currentEpoch) { + prob := ecfinality.CalcValidatorProb(chain, finality, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-i) _, _ = fmt.Fprintf(out, "%d,%d,%e\n", headEpoch-i, i, prob) } return nil @@ -166,31 +161,8 @@ machine-readable output of all 900 epochs.`, } // Bisect search for the finality threshold depth - thresholdDepth := -1 - low, high := bisectLow, min(bisectHigh, currentEpoch) + thresholdDepth := ecfinality.FindThresholdDepth(chain, finality, blocksPerEpoch, byzantineFraction, guarantee) - if low >= high { - // Chain too short for bisect search - thresholdDepth = -1 - } else if probLow := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-low); probLow < guarantee { - thresholdDepth = low - } else { - probHigh := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-high) - if probHigh > guarantee { - thresholdDepth = -1 - } else { - for low < high { - mid := (low + high) / 2 - prob := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-mid) - if prob < guarantee { - high = mid - } else { - low = mid + 1 - } - } - thresholdDepth = low - } - } // Print summary header _, _ = fmt.Fprintln(out, "EC Finality Calculator (FRC-0089)") _, _ = fmt.Fprintln(out) @@ -203,7 +175,7 @@ machine-readable output of all 900 epochs.`, if thresholdDepth > 0 { _, _ = fmt.Fprintf(out, " Probabilistic finality target (2^%d) reached at depth %d (%s)\n", safetyExponent, thresholdDepth, epochToTime(thresholdDepth)) } else { - _, _ = fmt.Fprintf(out, " Probabilistic finality target (2^%d) NOT reached within %d epochs (chain may be unhealthy)\n", safetyExponent, bisectHigh) + _, _ = fmt.Fprintf(out, " Probabilistic finality target (2^%d) NOT reached within %d epochs (chain may be unhealthy)\n", safetyExponent, ecfinality.BisectHigh) } _, _ = fmt.Fprintln(out) @@ -232,7 +204,7 @@ machine-readable output of all 900 epochs.`, tw := tablewriter.New( tablewriter.Col("Depth", tablewriter.RightAlign()), tablewriter.Col("Epoch", tablewriter.RightAlign()), - tablewriter.Col("Error Probability", tablewriter.RightAlign()), + tablewriter.Col("Reorg Probability", tablewriter.RightAlign()), tablewriter.Col("Status"), ) @@ -240,7 +212,7 @@ machine-readable output of all 900 epochs.`, if depth > currentEpoch { continue } - prob := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-depth) + prob := ecfinality.CalcValidatorProb(chain, finality, blocksPerEpoch, byzantineFraction, currentEpoch, currentEpoch-depth) status := "above target" if prob < guarantee { @@ -256,10 +228,10 @@ machine-readable output of all 900 epochs.`, } depthStr := fmt.Sprintf("%d (%s)", depth, epochToTime(depth)) - tw.Write(map[string]interface{}{ + tw.Write(map[string]any{ "Depth": depthStr, "Epoch": headEpoch - depth, - "Error Probability": probStr, + "Reorg Probability": probStr, "Status": status, }) } @@ -270,169 +242,3 @@ machine-readable output of all 900 epochs.`, return nil }, } - -// FinalityCalcValidator computes the probability that a previous blockchain tipset gets -// replaced. This is a Go port of the Python reference implementation from FRC-0089: -// https://github.com/consensus-shipyard/ec-finality-calculator (finality_calc_validator.py) -// -// Parameters: -// - chain: block counts per epoch (index 0 = earliest epoch) -// - blocksPerEpoch: expected blocks per epoch (5 for mainnet) -// - byzantineFraction: upper bound on adversarial power fraction (e.g. 0.3) -// - currentEpoch: index into chain for the current epoch -// - targetEpoch: index into chain for the epoch being evaluated -func FinalityCalcValidator(chain []int, blocksPerEpoch float64, byzantineFraction float64, currentEpoch int, targetEpoch int) float64 { - const negligibleThreshold = 1e-25 - - maxKL := 400 - maxKB := (currentEpoch - targetEpoch) * int(blocksPerEpoch) - maxKM := 400 - maxIM := 100 - - rateMaliciousBlocks := blocksPerEpoch * byzantineFraction - rateHonestBlocks := blocksPerEpoch - rateMaliciousBlocks - - // Compute L: adversarial lead distribution at target epoch - prL := make([]float64, maxKL+1) - - for k := 0; k <= maxKL; k++ { - sumExpectedAdversarialBlocksI := 0.0 - sumChainBlocksI := 0 - - for i := targetEpoch; i > max(0, currentEpoch-int(policy.ChainFinality)); i-- { - sumExpectedAdversarialBlocksI += rateMaliciousBlocks - sumChainBlocksI += chain[i-1] - prLi := poissonProb(sumExpectedAdversarialBlocksI, float64(k+sumChainBlocksI)) - prL[k] = max(prL[k], prLi) - } - if k > 1 && prL[k] < negligibleThreshold && prL[k] < prL[k-1] { - maxKL = k - prL = prL[:k+1] - break - } - } - - prL[0] += 1 - sumFloat64(prL) - - // Compute B: adversarial blocks during settlement period - prB := make([]float64, maxKB+1) - - for k := 0; k <= maxKB; k++ { - prB[k] = poissonProb(float64(currentEpoch-targetEpoch)*rateMaliciousBlocks, float64(k)) - - if k > 1 && prB[k] < negligibleThreshold && prB[k] < prB[k-1] { - maxKB = k - prB = prB[:k+1] - break - } - } - - // Compute M: adversarial mining advantage in the future (Skellam distribution) - prHgt0 := 1 - poissonProb(rateHonestBlocks, 0) - - expZ := 0.0 - for k := 0; k < int(4*blocksPerEpoch); k++ { - pmf := poissonProb(rateMaliciousBlocks, float64(k)) - expZ += ((rateHonestBlocks + float64(k)) / math.Pow(2, float64(k))) * pmf - } - - ratePublicChain := prHgt0 * expZ - - prM := make([]float64, maxKM+1) - for k := 0; k <= maxKM; k++ { - for i := maxIM; i > 0; i-- { - probMI := skellampmf.SkellamPMF(k, float64(i)*rateMaliciousBlocks, float64(i)*ratePublicChain) - - if probMI < negligibleThreshold && probMI < prM[k] { - break - } - prM[k] = max(prM[k], probMI) - } - - if k > 1 && prM[k] < negligibleThreshold && prM[k] < prM[k-1] { - maxKM = k - prM = prM[:k+1] - break - } - } - - prM[0] += 1 - sumFloat64(prM) - - // Compute error probability upper bound via convolution - cumsumL := cumsum(prL) - cumsumB := cumsum(prB) - cumsumM := cumsum(prM) - - k := sumInt(chain[targetEpoch:currentEpoch]) - - sumLgeK := cumsumL[len(cumsumL)-1] - if k > 0 { - sumLgeK -= cumsumL[min(k-1, maxKL)] - } - - doubleSum := 0.0 - - for l := range k { - sumBgeKminL := cumsumB[len(cumsumB)-1] - if k-l-1 > 0 { - sumBgeKminL -= cumsumB[min(k-l-1, maxKB)] - } - doubleSum += prL[min(l, maxKL)] * sumBgeKminL - - for b := 0; b < k-l; b++ { - sumMgeKminLminB := cumsumM[len(cumsumM)-1] - if k-l-b-1 > 0 { - sumMgeKminLminB -= cumsumM[min(k-l-b-1, maxKM)] - } - doubleSum += prL[min(l, maxKL)] * prB[min(b, maxKB)] * sumMgeKminLminB - } - } - - prError := sumLgeK + doubleSum - - return min(prError, 1.0) -} - -func poissonProb(lambda float64, x float64) float64 { - return math.Exp(poissonLogProb(lambda, x)) -} - -func poissonLogProb(lambda float64, x float64) float64 { - if x < 0 || math.Floor(x) != x { - return math.Inf(-1) - } - if lambda == 0 { - if x == 0 { - return 0 // P(X=0 | lambda=0) = 1, log(1) = 0 - } - return math.Inf(-1) - } - lg, _ := math.Lgamma(math.Floor(x) + 1) - return x*math.Log(lambda) - lambda - lg -} - -func sumFloat64(s []float64) float64 { - var total float64 - for _, v := range s { - total += v - } - return total -} - -func sumInt(s []int) int { - var total int - for _, v := range s { - total += v - } - return total -} - -func cumsum(arr []float64) []float64 { - result := make([]float64, len(arr)) - var s float64 - for i, v := range arr { - s += v - result[i] = s - } - return result -} diff --git a/documentation/en/api-methods-v2-experimental.md b/documentation/en/api-methods-v2-experimental.md index e3d79428fb5..0644311dcb5 100644 --- a/documentation/en/api-methods-v2-experimental.md +++ b/documentation/en/api-methods-v2-experimental.md @@ -1,6 +1,7 @@ # Groups * [Chain](#Chain) * [ChainGetTipSet](#ChainGetTipSet) + * [ChainGetTipSetFinalityStatus](#ChainGetTipSetFinalityStatus) * [Eth](#Eth) * [EthAccounts](#EthAccounts) * [EthAddressToFilecoinAddress](#EthAddressToFilecoinAddress) @@ -185,6 +186,304 @@ Response: } ``` +### ChainGetTipSetFinalityStatus +ChainGetTipSetFinalityStatus returns a breakdown of how the node is +currently determining finality. The result includes the EC probabilistic +finality depth (based on observed chain health), the F3-finalized tipset +(if available), and the overall finalized tipset the node is using. + +Useful for monitoring chain health and diagnosing finality lag. + +Experimental: This API is experimental and may change without notice. + + +Perms: read + +Inputs: `null` + +Response: +```json +{ + "ecFinalityThresholdDepth": 30, + "ecFinalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "f3FinalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "finalizedTipSet": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + }, + "head": { + "Cids": [ + { + "/": "bafy2bzacedo7hjsumaajt6sbor42qycvjyk6goqe4oi4o4ddsjxkdeqrqf42c" + } + ], + "Blocks": [ + { + "Miner": "f01938223", + "Ticket": { + "VRFProof": "rIPyBy+F827Szc5oN/6ylCmpzxfAWr7aI5F4YJrN4pLSyknkcJI3ivsCo2KKjQVZFRnFyEus1maD5LdzQpnFRKMla4138qEuML+Ne/fsgOMrUEAeL34ceVwJd+Mt4Jrz" + }, + "ElectionProof": { + "WinCount": 1, + "VRFProof": "sN51JqjZNf+xWxwoo+wlMH1bpXI9T3wUIrla6FpwTxU4jC1z+ab5NFU/B2ZdDITTE+u8qaiibtLkld5lhNcOEOUqwKNyJ4nwFo5vAhWqvOTNdOiZmxsKpWG0NZUoXb/+" + }, + "BeaconEntries": [ + { + "Round": 17133822, + "Data": "tH4q8euIaP9/QRJt8ALfkBvttSmQ/DOAt8+37wGGV5f8kkhzEFrHhskitNnPS70j" + }, + { + "Round": 17133832, + "Data": "uQD5cEn8U69+sPjpccT8Bm0jVrnXLScf2jBkLJNHvAHLA6tPsZDREzpBIckpVvPy" + } + ], + "WinPoStProof": [ + { + "PoStProof": 3, + "ProofBytes": "qOPLMhMui8qm/rE2y/UceyBDv5JvRCH5Fc5Ul+kuN190XDcMme5eKURUCmE2sN1HoQ2dMZX+xNZY351dbG93H/tUr6wuNhkvmemi2Xi62YvqU36/kJh+K2YBiW7h/4LXCUTP/6XAOONOPl+j9GqS7RQxruPLfIyehvzVC0C8dB8+SVWtAnRKRPUUOPJvyHKejlrCyzWXOz/I7JG2/qEGLD0xwazBVwML1vVvuE5NzXeOoQGlnB2PwSRb5Cn8FH8Q" + } + ], + "Parents": [ + { + "/": "bafy2bzaceba2kdmysmi5ieugzvv5np7f2lobayzpvtk777du74n7jq6xhynda" + }, + { + "/": "bafy2bzacecrye24tkqrvvddcf62gfi4z4o33z2tdedbpaalordozaxfrz2jyi" + }, + { + "/": "bafy2bzaceab5mrohjvnp3mz7mo33ky7qqlmssrs7veqmjrgouafxyhnd5dy66" + } + ], + "ParentWeight": "116013147118", + "Height": 4863283, + "ParentStateRoot": { + "/": "bafy2bzaceajxzsvzuq3ddzxfrs2jlaxsooqmgdy5uxbqujnjy3y56iumzzy7u" + }, + "ParentMessageReceipts": { + "/": "bafy2bzacecfcx2ykqucyv3gkyrcy3upwrvdraz3ktfg7phkqysefdwsggglac" + }, + "Messages": { + "/": "bafy2bzacebzofmh6migvc4v6qsme6vuxlhi6pv2ocy4apyic3uihjqm7dum3u" + }, + "BLSAggregate": { + "Type": 2, + "Data": "krFATGA0OBu/kFwtXsThVtKCkppnU7045uTURCeiOeJttxuXfx3wqJrLkCytnJFWFLVC+tiVWI4BxC3wqc9r6eAlNr9dEBx+3KwML/RFG/b5grmknLpGWn7g1EB/2T4y" + }, + "Timestamp": 1744204890, + "BlockSig": { + "Type": 2, + "Data": "pWiUr+M8xxTxLED7GuU586gSfZCaHyLbLj0uS0HhKYRtHuyG47fIrfIT/04OCmQvEXBD8pFraWbMc3tnFrSsM1mIBJ5M38UPUfXDSspo+QGdouo2kll2X+VNKY3ajb1K" + }, + "ForkSignaling": 0, + "ParentBaseFee": "20592036" + } + ], + "Height": 4863283 + } +} +``` + ## Eth These methods are used for Ethereum-compatible JSON-RPC calls diff --git a/documentation/en/finality.md b/documentation/en/finality.md new file mode 100644 index 00000000000..417876c3458 --- /dev/null +++ b/documentation/en/finality.md @@ -0,0 +1,79 @@ +# Finality in Filecoin + +This document explains how Lotus determines when a tipset is "finalized", meaning it will not be reorganized out of the canonical chain. + +## Overview + +Filecoin has two independent finality mechanisms that run in parallel: + +1. **F3 (Fast Finality)**: a BFT-style finality gadget that produces certificates for finalized tipsets. When running normally, F3 finalizes tipsets within 4-10 epochs. +2. **EC Probabilistic Finality**: based on observed block production, computes the probability that a confirmed tipset could be reorganized by an adversarial fork. Under healthy conditions (~5 blocks produced per tipset), the one-in-a-billion (2^-30) finality guarantee is typically achieved within ~30 epochs. + +Lotus takes the **most recent** finalized tipset from either source. If F3 is finalizing at depth 7 and the EC calculator says depth 22, F3 wins. If F3 is lagging at depth 40 but the calculator says depth 25, the calculator wins. + +If both are unavailable, Lotus falls back to the static 900-epoch EC finality assumption which represents an absolute worst-case chain health assumption given the one-in-a-billion finality guarantee. + +## What "finalized" and "safe" mean + +A **finalized** tipset has a negligible probability of being reverted. In Ethereum terminology, this maps to the `"finalized"` block tag. Applications that need strong settlement guarantees (bridges, exchanges, payment confirmations) should wait for finality before considering a transaction settled. + +The **safe** tag returns a tipset that balances recency with confidence, sitting between `"finalized"` and `"latest"`. It is defined as whichever is more recent: the finalized tipset, or the tipset at a fixed lookback distance from head (200 epochs for v2 / Eth v2, 30 epochs for Eth v1). Under normal conditions where F3 and/or the EC calculator are providing a finality depth shallower than the safe distance, `"safe"` and `"finalized"` will return the same tipset. When finality mechanisms are degraded or lagging, `"safe"` provides a more recent tipset than `"finalized"` at the cost of a weaker guarantee. + +Typically `"finalized"` is the right choice for most applications that don't want to be concerned with chain reorganization. + +## Querying finality + +### v2 API + +The `"finalized"` and `"safe"` tags can be used as tipset selectors: + +``` +Filecoin.ChainGetTipSet({"tag":"finalized"}) +Filecoin.ChainGetTipSet({"tag":"safe"}) +``` + +To understand how finality is currently being determined: + +``` +Filecoin.ChainGetTipSetFinalityStatus() +``` + +This returns: + +| Field | Description | +|-------|-------------| +| `ecFinalityThresholdDepth` | Epoch depth at which the reorg probability drops below 2^-30. -1 if chain health is too degraded. | +| `ecFinalizedTipSet` | The tipset at that depth (nil if threshold not met) | +| `f3FinalizedTipSet` | The tipset finalized by F3 (nil if F3 is unavailable) | +| `finalizedTipSet` | The overall finalized tipset the node is using | +| `head` | Current chain head | + +This endpoint is useful for monitoring and diagnostics: understanding *why* finality is at a particular depth, and whether F3 or the EC calculator is currently driving finalization. + +### Ethereum JSON-RPC + +``` +eth_getBlockByNumber("finalized", false) +eth_getBlockByNumber("safe", false) +``` + +The `"finalized"` and `"safe"` tags use the same parallel F3 + EC calculator logic described above. + +### lotus-shed + +```bash +lotus-shed finality-calculator +``` + +Displays a table of reorg probabilities at various depths for the current chain, along with the threshold depth where the 2^-30 guarantee is met. Supports `--csv` for machine-readable output. + +## How EC probabilistic finality works + +The EC calculator examines recent chain history (block counts per epoch over the last 900 epochs) and computes an upper bound on the probability that an adversary controlling up to 30% of mining power could build a longer fork that replaces a confirmed tipset. The algorithm is specified in [FRC-0089](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0089.md), with formal proofs and evaluation in ["The Finality Calculator: Analyzing and Quantifying Filecoin's Finality Guarantees"](https://arxiv.org/abs/2603.01307) (Goren & Soares, 2026). + +Key factors that affect the threshold depth: + +- **Block production rate**: more blocks per epoch means faster finality. Healthy mainnet (~4.5 blocks/epoch) typically reaches 2^-30 within 25-35 epochs. Degraded production (2-3 blocks/epoch) pushes the threshold deeper or may not reach it at all. +- **Adversary power assumption**: the standard assumption is 30% Byzantine power. This is the same assumption that the static 900-epoch finality was designed around. + +The calculator is at least as conservative as the static 900-epoch assumption. When chain health is poor, the threshold moves deeper (not shallower), and if conditions are severely degraded, the calculator reports that the threshold is not met (`ecFinalityThresholdDepth: -1`), causing the node to fall back to the static 900 epochs. Network partitions reduce observed block production, which makes the calculator *more* conservative, not less. diff --git a/gateway/proxy_v2.go b/gateway/proxy_v2.go index ba0bd230a98..dc205c40032 100644 --- a/gateway/proxy_v2.go +++ b/gateway/proxy_v2.go @@ -38,6 +38,13 @@ func (pv2 *reverseProxyV2) ChainGetTipSet(ctx context.Context, selector types.Ti return pv2.server.ChainGetTipSet(ctx, selector) } +func (pv2 *reverseProxyV2) ChainGetTipSetFinalityStatus(ctx context.Context) (*types.FinalityStatus, error) { + if err := pv2.gateway.limit(ctx, chainRateLimitTokens); err != nil { + return nil, err + } + return pv2.server.ChainGetTipSetFinalityStatus(ctx) +} + func (pv2 *reverseProxyV2) StateGetActor(ctx context.Context, address address.Address, selector types.TipSetSelector) (*types.Actor, error) { if err := pv2.gateway.limit(ctx, stateRateLimitTokens); err != nil { return nil, err diff --git a/itests/api_v2_test.go b/itests/api_v2_test.go index f79e9d76fe0..e6803bdeac7 100644 --- a/itests/api_v2_test.go +++ b/itests/api_v2_test.go @@ -34,7 +34,8 @@ func TestAPIV2_ThroughRPC(t *testing.T) { kit.QuietMiningLogs() mockF3 := kit.NewMockF3Backend() - subject, miner, network := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.F3Backend(mockF3)) + mockECFinality := kit.NewMockECFinalityProvider() + subject, miner, network := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.F3Backend(mockF3), kit.ECFinalityProvider(mockECFinality)) network.BeginMining(blockTime) subject.WaitTillChain(ctx, kit.HeightAtLeast(targetHeight)) @@ -285,6 +286,75 @@ func TestAPIV2_ThroughRPC(t *testing.T) { wantErr: "looking for tipset with height greater than start point", wantResponseStatus: http.StatusOK, }, + { + name: "ec calculator ahead of f3 uses ec finalized", + when: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, 50) // F3 at 50 + ecTipSet := tipSetAtHeight(200)(t) // EC calculator at 200 + mockECFinality.FinalizedTipSet = ecTipSet + mockECFinality.ThresholdDepth = int(targetHeight - 200) + mockECFinality.Err = nil + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`, + wantTipSet: tipSetAtHeight(200), + wantResponseStatus: http.StatusOK, + }, + { + name: "f3 ahead of ec calculator uses f3 finalized", + when: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, 500) // F3 at 500 + ecTipSet := tipSetAtHeight(200)(t) // EC calculator at 200 + mockECFinality.FinalizedTipSet = ecTipSet + mockECFinality.ThresholdDepth = int(targetHeight - 200) + mockECFinality.Err = nil + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`, + wantTipSet: tipSetAtHeight(500), + wantResponseStatus: http.StatusOK, + }, + { + name: "ec calculator error falls back to static ec finality", + when: func(t *testing.T) { + mockF3.Running = false + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = errors.New("calculator broken") + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`, + wantTipSet: ecFinalized, + wantResponseStatus: http.StatusOK, + }, + { + name: "ec calculator not met falls back to static ec finality", + when: func(t *testing.T) { + mockF3.Running = false + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = nil + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"finalized"}],"id":1}`, + wantTipSet: ecFinalized, + wantResponseStatus: http.StatusOK, + }, + { + name: "safe tag uses ec calculator when finalized beats safe distance", + when: func(t *testing.T) { + mockF3.Running = false + ecTipSet := tipSetAtHeight(900)(t) // well ahead of safe distance + mockECFinality.FinalizedTipSet = ecTipSet + mockECFinality.ThresholdDepth = int(targetHeight - 900) + mockECFinality.Err = nil + }, + request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"tag":"safe"}],"id":1}`, + wantTipSet: tipSetAtHeight(900), + wantResponseStatus: http.StatusOK, + }, { name: "height with anchor to latest", when: func(t *testing.T) { @@ -292,6 +362,9 @@ func TestAPIV2_ThroughRPC(t *testing.T) { mockF3.Finalizing = true mockF3.LatestCert = plausibleCertAt(t, f3FinalizedEpoch) mockF3.LatestCertErr = nil + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = nil }, request: `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSet","params":[{"height":{"at":890,"anchor":{"tag":"latest"}}}],"id":1}`, wantTipSet: tipSetAtHeight(890), @@ -446,6 +519,122 @@ func TestAPIV2_ThroughRPC(t *testing.T) { }) } }) + t.Run("ChainGetTipSetFinalityStatus", func(t *testing.T) { + for _, test := range []struct { + name string + when func(t *testing.T) + wantECThresholdDepth int + wantECFinalizedHeight abi.ChainEpoch // -1 means nil + wantF3FinalizedHeight abi.ChainEpoch // -1 means nil + wantFinalizedHeight abi.ChainEpoch // -1 means check non-nil but dynamic + }{ + { + name: "ec calculator with f3 ahead", + when: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, 500) + ecTipSet := tipSetAtHeight(200)(t) + mockECFinality.FinalizedTipSet = ecTipSet + mockECFinality.ThresholdDepth = int(targetHeight - 200) + mockECFinality.Err = nil + }, + wantECThresholdDepth: int(targetHeight - 200), + wantECFinalizedHeight: 200, + wantF3FinalizedHeight: 500, + wantFinalizedHeight: 500, + }, + { + name: "ec calculator ahead of f3", + when: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, 50) + ecTipSet := tipSetAtHeight(200)(t) + mockECFinality.FinalizedTipSet = ecTipSet + mockECFinality.ThresholdDepth = int(targetHeight - 200) + mockECFinality.Err = nil + }, + wantECThresholdDepth: int(targetHeight - 200), + wantECFinalizedHeight: 200, + wantF3FinalizedHeight: 50, + wantFinalizedHeight: 200, + }, + { + name: "ec calculator not met and no f3 falls back to static finality", + when: func(t *testing.T) { + mockF3.Running = false + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = nil + }, + wantECThresholdDepth: -1, + wantECFinalizedHeight: -1, + wantF3FinalizedHeight: -1, + wantFinalizedHeight: -1, // dynamic: head - policy.ChainFinality + }, + { + name: "ec calculator error falls back to static finality", + when: func(t *testing.T) { + mockF3.Running = false + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = errors.New("calculator broken") + }, + wantECThresholdDepth: -1, + wantECFinalizedHeight: -1, + wantF3FinalizedHeight: -1, + wantFinalizedHeight: -1, // dynamic: head - policy.ChainFinality + }, + } { + t.Run(test.name, func(t *testing.T) { + if test.when != nil { + test.when(t) + } + + var gotResponseCode int + var gotResponseBody []byte + var resultOrError struct { + Result *types.FinalityStatus `json:"result,omitempty"` + Error *struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` + } `json:"error,omitempty"` + } + + gotResponseCode, gotResponseBody = subject.DoRawRPCRequest(t, 2, `{"jsonrpc":"2.0","method":"Filecoin.ChainGetTipSetFinalityStatus","params":[],"id":1}`) + require.Equal(t, http.StatusOK, gotResponseCode, string(gotResponseBody)) + require.NoError(t, json.Unmarshal(gotResponseBody, &resultOrError)) + require.Nil(t, resultOrError.Error) + require.NotNil(t, resultOrError.Result) + status := resultOrError.Result + + require.NotNil(t, status.Head, "Head should always be set") + require.Equal(t, test.wantECThresholdDepth, status.ECFinalityThresholdDepth) + + if test.wantECFinalizedHeight >= 0 { + require.NotNil(t, status.ECFinalizedTipSet) + require.Equal(t, test.wantECFinalizedHeight, status.ECFinalizedTipSet.Height()) + } else { + require.Nil(t, status.ECFinalizedTipSet) + } + + if test.wantF3FinalizedHeight >= 0 { + require.NotNil(t, status.F3FinalizedTipSet) + require.Equal(t, test.wantF3FinalizedHeight, status.F3FinalizedTipSet.Height()) + } else { + require.Nil(t, status.F3FinalizedTipSet) + } + + require.NotNil(t, status.FinalizedTipSet, "FinalizedTipSet should always be set") + if test.wantFinalizedHeight >= 0 { + require.Equal(t, test.wantFinalizedHeight, status.FinalizedTipSet.Height()) + } + }) + } + }) t.Run("StateGetID", func(t *testing.T) { for _, test := range []struct { name string diff --git a/itests/eth_api_f3_test.go b/itests/eth_api_f3_test.go index 8107980cba4..2a8f3532537 100644 --- a/itests/eth_api_f3_test.go +++ b/itests/eth_api_f3_test.go @@ -59,7 +59,8 @@ func TestEthAPIWithF3(t *testing.T) { kit.QuietMiningLogs() mockF3 := kit.NewMockF3Backend() - client, miner, network := kit.EnsembleMinimal(t, kit.F3Backend(mockF3), kit.MockProofs()) + mockECFinality := kit.NewMockECFinalityProvider() + client, miner, network := kit.EnsembleMinimal(t, kit.F3Backend(mockF3), kit.ECFinalityProvider(mockECFinality), kit.MockProofs()) network.BeginMining(blockTime) _, fundedEthAddr, fundedFilAddr := client.EVM().NewAccount() @@ -390,6 +391,91 @@ func TestEthAPIWithF3(t *testing.T) { wantErrV1: "decoding latest f3 cert tipset key", wantErrV2: "decoding latest f3 cert tipset key", }, + { + name: "finalized tag when ec calculator ahead of f3 uses ec finalized", + blkParam: "finalized", + setup: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, 50) // F3 at 50 + ecTipSet := tipSetAtHeight(f3FinalizedEpoch)(t) + mockECFinality.FinalizedTipSet = ecTipSet // EC calc at f3FinalizedEpoch (300) + mockECFinality.ThresholdDepth = int(targetHeight - f3FinalizedEpoch) + mockECFinality.Err = nil + }, + wantTipSetV1: tipSetAtHeight(f3FinalizedEpoch), + wantTipSetV2: tipSetAtHeight(f3FinalizedEpoch), + }, + { + name: "finalized tag when f3 ahead of ec calculator uses f3 finalized", + blkParam: "finalized", + setup: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, f3FinalizedEpoch) // F3 at 300 + ecTipSet := tipSetAtHeight(50)(t) + mockECFinality.FinalizedTipSet = ecTipSet // EC calc at 50 + mockECFinality.ThresholdDepth = int(targetHeight - 50) + mockECFinality.Err = nil + }, + wantTipSetV1: tipSetAtHeight(f3FinalizedEpoch), + wantTipSetV2: tipSetAtHeight(f3FinalizedEpoch), + }, + { + name: "finalized tag when ec calculator error falls back to static ec finality", + blkParam: "finalized", + setup: func(t *testing.T) { + mockF3.Running = false + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = errors.New("calculator broken") + }, + wantTipSetV1: ecFinalized, + wantTipSetV2: ecFinalized, + }, + { + name: "finalized tag when ec calculator not met falls back to static ec finality", + blkParam: "finalized", + setup: func(t *testing.T) { + mockF3.Running = false + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = nil + }, + wantTipSetV1: ecFinalized, + wantTipSetV2: ecFinalized, + }, + { + name: "safe tag uses ec calculator when finalized beats safe distance with f3", + blkParam: "safe", + setup: func(t *testing.T) { + mockF3.Running = true + mockF3.Finalizing = true + mockF3.LatestCertErr = nil + mockF3.LatestCert = plausibleCertAt(t, targetHeight) // F3 at head + ecTipSet := tipSetAtHeight(targetHeight)(t) + mockECFinality.FinalizedTipSet = ecTipSet // EC calc at head + mockECFinality.ThresholdDepth = 0 + mockECFinality.Err = nil + }, + wantTipSetV1: tipSetAtHeight(targetHeight), + wantTipSetV2: tipSetAtHeight(targetHeight), + }, + { + name: "safe tag uses ec calculator when finalized beats safe distance without f3", + blkParam: "safe", + setup: func(t *testing.T) { + mockF3.Running = false + ecTipSet := tipSetAtHeight(targetHeight - 20)(t) // EC calc at depth ~20 + mockECFinality.FinalizedTipSet = ecTipSet + mockECFinality.ThresholdDepth = 20 + mockECFinality.Err = nil + }, + wantTipSetV1: tipSetAtHeight(targetHeight - 20), + wantTipSetV2: tipSetAtHeight(targetHeight - 20), + }, { name: "height before ec finalized epoch is ok", blkParam: fmt.Sprintf("0x%x", int(tipSetAtHeight(10)(t).Height())), @@ -398,6 +484,9 @@ func TestEthAPIWithF3(t *testing.T) { mockF3.Finalizing = true mockF3.LatestCert = plausibleCertAt(t, f3FinalizedEpoch) mockF3.LatestCertErr = nil + mockECFinality.FinalizedTipSet = nil + mockECFinality.ThresholdDepth = -1 + mockECFinality.Err = nil }, wantTipSetV1: tipSetAtHeight(10), wantTipSetV2: tipSetAtHeight(10), diff --git a/itests/kit/ec_finality.go b/itests/kit/ec_finality.go new file mode 100644 index 00000000000..d1dd5e6318e --- /dev/null +++ b/itests/kit/ec_finality.go @@ -0,0 +1,62 @@ +package kit + +import ( + "context" + + "github.com/filecoin-project/lotus/chain/ecfinality" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/node" + "github.com/filecoin-project/lotus/node/impl/eth" +) + +// MockECFinalityProvider satisfies ecfinality.ECFinalityCalculator (for ChainModuleV2) and +// eth.ECFinalityProvider (for tipSetResolver), allowing tests to control the +// EC finality calculator response without the additional cost of running the +// FRC-0089 calculator across 905 tipsets on every head change when the block +// time is tiny. +// +// Set FinalizedTipSet and ThresholdDepth to control the response. Set Err to +// simulate a calculator failure. +type MockECFinalityProvider struct { + FinalizedTipSet *types.TipSet + ThresholdDepth int + Err error +} + +func NewMockECFinalityProvider() *MockECFinalityProvider { + return &MockECFinalityProvider{ + ThresholdDepth: -1, + } +} + +func (m *MockECFinalityProvider) GetFinalizedTipSet(_ context.Context) (*types.TipSet, error) { + if m.Err != nil { + return nil, m.Err + } + return m.FinalizedTipSet, nil +} + +func (m *MockECFinalityProvider) GetStatus(_ context.Context) (*ecfinality.ECFinalityStatus, error) { + if m.Err != nil { + return nil, m.Err + } + return &ecfinality.ECFinalityStatus{ + ThresholdDepth: m.ThresholdDepth, + FinalizedTipSet: m.FinalizedTipSet, + }, nil +} + +var ( + _ ecfinality.ECFinalityCalculator = (*MockECFinalityProvider)(nil) + _ eth.ECFinalityProvider = (*MockECFinalityProvider)(nil) +) + +// ECFinalityProvider overrides the EC finality calculator used by the test +// node with the given mock, replacing the real ECFinalityCache which walks +// 905 tipsets on every head change. +func ECFinalityProvider(provider *MockECFinalityProvider) NodeOpt { + return ConstructorOpts( + node.Override(new(ecfinality.ECFinalityCalculator), provider), + node.Override(new(eth.ECFinalityProvider), provider), + ) +} diff --git a/itests/kit/node_opts.go b/itests/kit/node_opts.go index f4048fcc363..736ad12a56f 100644 --- a/itests/kit/node_opts.go +++ b/itests/kit/node_opts.go @@ -13,11 +13,13 @@ import ( "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/lotus/build/buildconstants" + "github.com/filecoin-project/lotus/chain/ecfinality" "github.com/filecoin-project/lotus/chain/lf3" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/wallet/key" "github.com/filecoin-project/lotus/node" "github.com/filecoin-project/lotus/node/config" + "github.com/filecoin-project/lotus/node/impl/eth" "github.com/filecoin-project/lotus/node/impl/full" "github.com/filecoin-project/lotus/node/modules" "github.com/filecoin-project/lotus/node/modules/dtypes" @@ -112,6 +114,16 @@ var DefaultNodeOpts = nodeOpts{ workerTasks: []sealtasks.TaskType{sealtasks.TTFetch, sealtasks.TTCommit1, sealtasks.TTFinalize, sealtasks.TTFinalizeUnsealed}, workerStorageOpt: func(store paths.Store) paths.Store { return store }, + + // Disable the EC finality calculator by default. In test environments + // with 1-2 miners, chain health is poor so the calculator either won't + // meet its threshold or returns a depth near the static 900 fallback. + // Tests that need EC finality can use ECFinalityProvider() to inject a + // mock with controlled values. + extraNodeOpts: []node.Option{ + node.Override(new(ecfinality.ECFinalityCalculator), func() ecfinality.ECFinalityCalculator { return nil }), + node.Override(new(eth.ECFinalityProvider), func() eth.ECFinalityProvider { return nil }), + }, } // OptBuilder is used to create an option after some other node is already diff --git a/node/builder_chain.go b/node/builder_chain.go index 4bcaf578b37..aecd399c63f 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -13,9 +13,11 @@ import ( "github.com/filecoin-project/lotus/api/v2api" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain" + "github.com/filecoin-project/lotus/chain/actors/policy" "github.com/filecoin-project/lotus/chain/beacon" "github.com/filecoin-project/lotus/chain/consensus" "github.com/filecoin-project/lotus/chain/consensus/filcns" + "github.com/filecoin-project/lotus/chain/ecfinality" "github.com/filecoin-project/lotus/chain/events" "github.com/filecoin-project/lotus/chain/events/filter" "github.com/filecoin-project/lotus/chain/exchange" @@ -166,6 +168,10 @@ var ChainNode = Options( Override(new(messagepool.MpoolNonceAPI), From(new(*messagepool.MessagePool))), Override(new(full.ChainModuleAPI), From(new(full.ChainModule))), Override(new(full.ChainModuleAPIv2), From(new(full.ChainModuleV2))), + Override(new(ecfinality.ECFinalityCalculator), func(cs *store.ChainStore) ecfinality.ECFinalityCalculator { + return ecfinality.NewECFinalityCache(cs, int(policy.ChainFinality)) + }), + Override(new(eth.ECFinalityProvider), From(new(ecfinality.ECFinalityCalculator))), Override(new(full.GasModuleAPI), From(new(full.GasModule))), Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))), Override(new(full.StateModuleAPI), From(new(full.StateModule))), diff --git a/node/impl/eth/tipsetresolver.go b/node/impl/eth/tipsetresolver.go index e911a1981cd..7a91eeefefa 100644 --- a/node/impl/eth/tipsetresolver.go +++ b/node/impl/eth/tipsetresolver.go @@ -23,14 +23,22 @@ type F3CertificateProvider interface { F3GetLatestCertificate(ctx context.Context) (*certs.FinalityCertificate, error) } +// ECFinalityProvider provides a probabilistic EC finality tipset based on the +// FRC-0089 calculator. The provider may return nil if the finality threshold is +// not met within the search range. +type ECFinalityProvider interface { + GetFinalizedTipSet(ctx context.Context) (*types.TipSet, error) +} + type tipSetResolver struct { cs ChainStore f3 F3CertificateProvider // can be nil if disabled + ecFinality ECFinalityProvider // can be nil if disabled useF3ForFinality bool // if true, attempt to use F3 to determine "finalized" tipset } -func NewTipSetResolver(cs ChainStore, f3 F3CertificateProvider, useF3ForFinality bool) TipSetResolver { - return &tipSetResolver{cs: cs, f3: f3, useF3ForFinality: useF3ForFinality} +func NewTipSetResolver(cs ChainStore, f3 F3CertificateProvider, ecFinality ECFinalityProvider, useF3ForFinality bool) TipSetResolver { + return &tipSetResolver{cs: cs, f3: f3, ecFinality: ecFinality, useF3ForFinality: useF3ForFinality} } func (tsr *tipSetResolver) getLatestF3Cert(ctx context.Context) (*certs.FinalityCertificate, error) { @@ -63,55 +71,53 @@ func (tsr *tipSetResolver) getFinalizedF3TipSetFromCert(ctx context.Context, cer } func (tsr *tipSetResolver) getSafeF3TipSet(ctx context.Context) (*types.TipSet, error) { - // To determine the safe tipset, we check the finalized F3 tipset and compare that to the tipset - // we consider safe from EC and return the higher of the two. - var f3Ts *types.TipSet - cert, err := tsr.getLatestF3Cert(ctx) + // Compute the full finalized tipset (EC calculator + F3, whichever is more recent), then compare + // with the static safe distance. If finalization is ahead of the safe distance, return the + // finalized tipset since it provides a stronger guarantee. + finalized, err := tsr.getFinalizedF3TipSet(ctx) if err != nil { return nil, err - } else if cert != nil { - f3Ts, err = tsr.getFinalizedF3TipSetFromCert(ctx, cert) - if err != nil { - return nil, err - } - } // else F3 is disabled or not ready - ecTs, err := tsr.getSafeECTipSet(ctx) + } + ecSafe, err := tsr.getSafeECTipSet(ctx) if err != nil { return nil, err } - if f3Ts == nil || f3Ts.Height() < ecTs.Height() { - return ecTs, nil + if finalized != nil && finalized.Height() >= ecSafe.Height() { + return finalized, nil } - // F3 is finalizing a higher height than EC safe; return F3 tipset - return f3Ts, nil + return ecSafe, nil } func (tsr *tipSetResolver) getFinalizedF3TipSet(ctx context.Context) (*types.TipSet, error) { - cert, err := tsr.getLatestF3Cert(ctx) + // Always compute EC finality so it can be compared with F3. + ecTs, err := tsr.getFinalizedECTipSet(ctx) if err != nil { return nil, err - } else if cert == nil { - // F3 is disabled or not ready; fall back to EC finality. - return tsr.getFinalizedECTipSet(ctx) } - // Check F3 finalized tipset against the heaviest tipset, and if it is too far - // behind fall back to EC. - head := tsr.cs.GetHeaviestTipSet() - if head == nil { - return nil, xerrors.Errorf("no known heaviest tipset") + + cert, err := tsr.getLatestF3Cert(ctx) + if err != nil { + return nil, err } - f3FinalizedHeight := abi.ChainEpoch(cert.ECChain.Head().Epoch) - if head.Height()-f3FinalizedHeight > policy.ChainFinality { - log.Debugw("Falling back to EC finalized tipset as the latest F3 finalized tipset is too far behind", "headHeight", head.Height(), "f3FinalizedHeight", f3FinalizedHeight) - return tsr.getFinalizedECTipSet(ctx) + + // If not operating, or the F3 finalized tipset is behind EC finality, just use EC finality. + if cert == nil || abi.ChainEpoch(cert.ECChain.Head().Epoch) <= ecTs.Height() { + return ecTs, nil } - // F3 is finalizing a higher height than EC safe; return F3 tipset + return tsr.getFinalizedF3TipSetFromCert(ctx, cert) } func (tsr *tipSetResolver) getSafeECTipSet(ctx context.Context) (*types.TipSet, error) { + finalized, err := tsr.getFinalizedECTipSet(ctx) + if err != nil { + return nil, err + } + head := tsr.cs.GetHeaviestTipSet() - height := head.Height() + if head == nil { + return nil, xerrors.Errorf("no known heaviest tipset") + } // Default safe distance (used for v2 APIs, or v1 APIs when F3 is finalizing) safeDistance := buildconstants.SafeHeightDistance @@ -122,12 +128,26 @@ func (tsr *tipSetResolver) getSafeECTipSet(ctx context.Context) (*types.TipSet, safeDistance = ethtypes.SafeEpochDelay } - safeHeight := max(0, height-safeDistance) + safeHeight := max(0, head.Height()-safeDistance) + if finalized != nil && finalized.Height() >= safeHeight { + return finalized, nil + } return tsr.cs.GetTipsetByHeight(ctx, safeHeight, head, true) } func (tsr *tipSetResolver) getFinalizedECTipSet(ctx context.Context) (*types.TipSet, error) { + if tsr.ecFinality != nil { + ts, err := tsr.ecFinality.GetFinalizedTipSet(ctx) + if err != nil { + log.Debugw("EC finality calculator error, falling back to static finality", "err", err) + } else if ts != nil { + return ts, nil + } + } head := tsr.cs.GetHeaviestTipSet() + if head == nil { + return nil, xerrors.Errorf("no known heaviest tipset") + } height := max(0, head.Height()-policy.ChainFinality) return tsr.cs.GetTipsetByHeight(ctx, height, head, true) } diff --git a/node/impl/full/chain_v2.go b/node/impl/full/chain_v2.go index 71d0f060685..260dda1122d 100644 --- a/node/impl/full/chain_v2.go +++ b/node/impl/full/chain_v2.go @@ -13,6 +13,7 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build/buildconstants" "github.com/filecoin-project/lotus/chain/actors/policy" + "github.com/filecoin-project/lotus/chain/ecfinality" "github.com/filecoin-project/lotus/chain/lf3" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" @@ -22,11 +23,13 @@ var _ ChainModuleAPIv2 = (*ChainModuleV2)(nil) type ChainModuleAPIv2 interface { ChainGetTipSet(context.Context, types.TipSetSelector) (*types.TipSet, error) + ChainGetTipSetFinalityStatus(context.Context) (*types.FinalityStatus, error) } type ChainModuleV2 struct { - Chain *store.ChainStore - F3 lf3.F3Backend `optional:"true"` + Chain *store.ChainStore + F3 lf3.F3Backend `optional:"true"` + ECFinality ecfinality.ECFinalityCalculator `optional:"true"` fx.In } @@ -58,6 +61,71 @@ func (cm *ChainModuleV2) ChainGetTipSet(ctx context.Context, selector types.TipS return nil, xerrors.Errorf("no tipset found for selector") } +func (cm *ChainModuleV2) ChainGetTipSetFinalityStatus(ctx context.Context) (*types.FinalityStatus, error) { + head := cm.Chain.GetHeaviestTipSet() + if head == nil { + return nil, xerrors.New("no known heaviest tipset") + } + + status := &types.FinalityStatus{ + Head: head, + ECFinalityThresholdDepth: -1, + } + + // EC calculator result + if cm.ECFinality != nil { + ecStatus, err := cm.ECFinality.GetStatus(ctx) + if err != nil { + log.Debugw("EC finality calculator error in status query", "err", err) + } else { + status.ECFinalityThresholdDepth = ecStatus.ThresholdDepth + status.ECFinalizedTipSet = ecStatus.FinalizedTipSet + } + } + + // EC finalized tipset: use the calculator result if available, otherwise + // fall back to static head - policy.ChainFinality. + ecFinalized := status.ECFinalizedTipSet + if ecFinalized == nil { + finalizedHeight := max(0, head.Height()-policy.ChainFinality) + ts, err := cm.Chain.GetTipsetByHeight(ctx, finalizedHeight, head, true) + if err != nil { + return nil, xerrors.Errorf("getting static EC finalized tipset: %w", err) + } + ecFinalized = ts + } + status.FinalizedTipSet = ecFinalized + + // F3 result: if F3 is ahead of EC, use that as the overall finalized tipset. + if cm.F3 != nil { + cert, err := cm.F3.GetLatestCert(ctx) + if err != nil { + if !errors.Is(err, f3.ErrF3NotRunning) && !errors.Is(err, api.ErrF3NotReady) { + return nil, xerrors.Errorf("getting F3 certificate: %w", err) + } + } else if cert != nil { + f3Epoch := abi.ChainEpoch(cert.ECChain.Head().Epoch) + // Only use F3 if it's within a plausible range of EC finality. + if head.Height()-f3Epoch <= policy.ChainFinality { + tsk, err := types.TipSetKeyFromBytes(cert.ECChain.Head().Key) + if err != nil { + return nil, xerrors.Errorf("decoding F3 cert tipset key: %w", err) + } + f3Ts, err := cm.Chain.LoadTipSet(ctx, tsk) + if err != nil { + return nil, xerrors.Errorf("loading F3 cert tipset %s: %w", tsk, err) + } + status.F3FinalizedTipSet = f3Ts + if f3Ts.Height() > status.FinalizedTipSet.Height() { + status.FinalizedTipSet = f3Ts + } + } + } + } + + return status, nil +} + func (cm *ChainModuleV2) getTipSetByTag(ctx context.Context, tag types.TipSetTag) (*types.TipSet, error) { switch tag { case types.TipSetTags.Latest: @@ -88,49 +156,41 @@ func (cm *ChainModuleV2) getLatestSafeTipSet(ctx context.Context) (*types.TipSet } func (cm *ChainModuleV2) getLatestFinalizedTipset(ctx context.Context) (*types.TipSet, error) { + // EC finality: use the FRC-0089 calculator when available, otherwise static + // head-900 fallback. This is always computed so it can be compared with F3. + ecTs, err := cm.getECFinalized(ctx) + if err != nil { + return nil, err + } + if cm.F3 == nil { // F3 is disabled; fall back to EC finality. - return cm.getECFinalized(ctx) + return ecTs, nil } 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) + log.Debugw("F3 not running or not ready, using EC finality", "err", err) + return ecTs, nil } 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. - latestF3FinalizedTipSet := cert.ECChain.Head() - - // Fall back to EC finality if the latest F3 finalized tipset is older than EC finality. - latestF3FinalizedEpoch := abi.ChainEpoch(latestF3FinalizedTipSet.Epoch) - head := cm.Chain.GetHeaviestTipSet() - if head == nil { - return nil, xerrors.New("no known heaviest tipset") - } - if head.Height()-latestF3FinalizedEpoch > policy.ChainFinality { - log.Debugw("Latest F3 finalized tipset is older than EC finality, falling back to EC finality", "headEpoch", head.Height(), "latestF3FinalizedEpoch", latestF3FinalizedEpoch) - return cm.getECFinalized(ctx) + // If not operating, or the F3 finalized tipset is behind EC finality, just use EC finality. + if cert == nil || abi.ChainEpoch(cert.ECChain.Head().Epoch) <= ecTs.Height() { + return ecTs, nil } - // All good, load the latest F3 finalized tipset. - tsk, err := types.TipSetKeyFromBytes(latestF3FinalizedTipSet.Key) + 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) + f3Ts, 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 + return f3Ts, nil } func (cm *ChainModuleV2) getTipSetByAnchor(ctx context.Context, anchor *types.TipSetAnchor) (*types.TipSet, error) { @@ -152,7 +212,20 @@ func (cm *ChainModuleV2) getTipSetByAnchor(ctx context.Context, anchor *types.Ti } func (cm *ChainModuleV2) getECFinalized(ctx context.Context) (*types.TipSet, error) { + // Use the FRC-0089 calculator for probabilistic EC finality when available. + if cm.ECFinality != nil { + ts, err := cm.ECFinality.GetFinalizedTipSet(ctx) + if err != nil { + log.Debugw("EC finality calculator error, falling back to static finality", "err", err) + } else if ts != nil { + return ts, nil + } + // Calculator returned nil (threshold not met), fall through to static. + } head := cm.Chain.GetHeaviestTipSet() + if head == nil { + return nil, xerrors.New("no known heaviest tipset") + } finalizedHeight := max(0, head.Height()-policy.ChainFinality) return cm.Chain.GetTipsetByHeight(ctx, finalizedHeight, head, true) } diff --git a/node/modules/eth.go b/node/modules/eth.go index 7d69b0ebc6e..224417a237e 100644 --- a/node/modules/eth.go +++ b/node/modules/eth.go @@ -29,7 +29,8 @@ import ( type TipSetResolverParams struct { fx.In ChainStore eth.ChainStore - F3 full.F3ModuleAPI `optional:"true"` + F3 full.F3ModuleAPI `optional:"true"` + ECFinality eth.ECFinalityProvider `optional:"true"` } func MakeV1TipSetResolver(params TipSetResolverParams) full.EthTipSetResolverV2 { @@ -44,11 +45,11 @@ func MakeV1TipSetResolver(params TipSetResolverParams) full.EthTipSetResolverV2 f3CertificateProvider = nil useF3ForFinality = false } - return eth.NewTipSetResolver(params.ChainStore, f3CertificateProvider, useF3ForFinality) + return eth.NewTipSetResolver(params.ChainStore, f3CertificateProvider, params.ECFinality, useF3ForFinality) } func MakeV2TipSetResolver(params TipSetResolverParams) full.EthTipSetResolverV2 { - return eth.NewTipSetResolver(params.ChainStore, params.F3, true) + return eth.NewTipSetResolver(params.ChainStore, params.F3, params.ECFinality, true) } func MakeEthFilecoinV1(stateManager eth.StateManager, tipsetResolver full.EthTipSetResolverV1) full.EthFilecoinAPIV1 {