Skip to content

Commit

Permalink
feat(f3): resolve finality for eth APIs according to F3
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Dec 5, 2024
1 parent 12d76bd commit cd0f34f
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 149 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# UNRELEASED

* **Ethereum APIs meet F3!** When F3 is enabled and running, Ethereum APIs that accept block descriptors `"finalized"` and `"safe"` will use F3 to determine the block to select instead of the default 30 block delay for `"safe"` and 900 block delay for `"finalized"`. ([filecoin-project/lotus#12760](https://github.com/filecoin-project/lotus/pull/12760))

# UNRELEASED v1.32.0

See https://github.com/filecoin-project/lotus/blob/release/v1.32.0/CHANGELOG.md
Expand Down
70 changes: 60 additions & 10 deletions chain/lf3/f3.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ import (
"github.com/filecoin-project/lotus/node/repo"
)

type F3API interface {
GetOrRenewParticipationTicket(ctx context.Context, minerID uint64, previous api.F3ParticipationTicket, instances uint64) (api.F3ParticipationTicket, error)
Participate(ctx context.Context, ticket api.F3ParticipationTicket) (api.F3ParticipationLease, error)
GetCert(ctx context.Context, instance uint64) (*certs.FinalityCertificate, error)
GetLatestCert(ctx context.Context) (*certs.FinalityCertificate, error)
GetManifest(ctx context.Context) (*manifest.Manifest, error)
GetPowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error)
GetF3PowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error)
IsEnabled() bool
IsRunning() (bool, error)
Progress() (gpbft.Instant, error)
ListParticipants() ([]api.F3Participant, error)
}

type F3 struct {
inner *f3.F3
ec *ecWrapper
Expand All @@ -37,6 +51,8 @@ type F3 struct {
leaser *leaser
}

var _ F3API = (*F3)(nil)

type F3Params struct {
fx.In

Expand Down Expand Up @@ -184,20 +200,20 @@ func (fff *F3) GetLatestCert(ctx context.Context) (*certs.FinalityCertificate, e
return fff.inner.GetLatestCert(ctx)
}

func (fff *F3) GetManifest(ctx context.Context) *manifest.Manifest {
func (fff *F3) GetManifest(ctx context.Context) (*manifest.Manifest, error) {
m := fff.inner.Manifest()
if m.InitialPowerTable.Defined() {
return m
return m, nil
}
cert0, err := fff.inner.GetCert(ctx, 0)
if err != nil {
return m
return m, nil
}

var mCopy = *m
m = &mCopy
m.InitialPowerTable = cert0.ECChain.Base().PowerTable
return m
return m, nil
}

func (fff *F3) GetPowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error) {
Expand All @@ -208,15 +224,19 @@ func (fff *F3) GetF3PowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.
return fff.inner.GetPowerTable(ctx, tsk.Bytes())
}

func (fff *F3) IsRunning() bool {
return fff.inner.IsRunning()
func (fff *F3) IsEnabled() bool {
return true
}

func (fff *F3) IsRunning() (bool, error) {
return fff.inner.IsRunning(), nil
}

func (fff *F3) Progress() gpbft.Instant {
return fff.inner.Progress()
func (fff *F3) Progress() (gpbft.Instant, error) {
return fff.inner.Progress(), nil
}

func (fff *F3) ListParticipants() []api.F3Participant {
func (fff *F3) ListParticipants() ([]api.F3Participant, error) {
leases := fff.leaser.getValidLeases()
participants := make([]api.F3Participant, len(leases))
for i, lease := range leases {
Expand All @@ -226,5 +246,35 @@ func (fff *F3) ListParticipants() []api.F3Participant {
ValidityTerm: lease.ValidityTerm,
}
}
return participants
return participants, nil
}

type DisabledF3 struct{}

var _ F3API = DisabledF3{}

func (DisabledF3) GetOrRenewParticipationTicket(_ context.Context, _ uint64, _ api.F3ParticipationTicket, _ uint64) (api.F3ParticipationTicket, error) {
return api.F3ParticipationTicket{}, api.ErrF3Disabled
}
func (DisabledF3) Participate(_ context.Context, _ api.F3ParticipationTicket) (api.F3ParticipationLease, error) {
return api.F3ParticipationLease{}, api.ErrF3Disabled
}
func (DisabledF3) GetCert(_ context.Context, _ uint64) (*certs.FinalityCertificate, error) {
return nil, api.ErrF3Disabled
}
func (DisabledF3) GetLatestCert(_ context.Context) (*certs.FinalityCertificate, error) {
return nil, api.ErrF3Disabled
}
func (DisabledF3) GetManifest(_ context.Context) (*manifest.Manifest, error) {
return nil, api.ErrF3Disabled
}
func (DisabledF3) GetPowerTable(_ context.Context, _ types.TipSetKey) (gpbft.PowerEntries, error) {
return nil, api.ErrF3Disabled
}
func (DisabledF3) GetF3PowerTable(_ context.Context, _ types.TipSetKey) (gpbft.PowerEntries, error) {
return nil, api.ErrF3Disabled
}
func (DisabledF3) IsEnabled() bool { return false }
func (DisabledF3) IsRunning() (bool, error) { return false, api.ErrF3Disabled }
func (DisabledF3) Progress() (gpbft.Instant, error) { return gpbft.Instant{}, api.ErrF3Disabled }
func (DisabledF3) ListParticipants() ([]api.F3Participant, error) { return nil, api.ErrF3Disabled }
97 changes: 97 additions & 0 deletions chain/lf3/mock/mock_f3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package mock

import (
"context"
"sync"

"github.com/filecoin-project/go-f3/certs"
"github.com/filecoin-project/go-f3/gpbft"
"github.com/filecoin-project/go-f3/manifest"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/lf3"
"github.com/filecoin-project/lotus/chain/types"
)

type MockF3API struct {
lk sync.Mutex

latestCert *certs.FinalityCertificate
enabled bool
running bool
}

func (m *MockF3API) GetOrRenewParticipationTicket(ctx context.Context, minerID uint64, previous api.F3ParticipationTicket, instances uint64) (api.F3ParticipationTicket, error) {
return api.F3ParticipationTicket{}, nil
}

func (m *MockF3API) Participate(ctx context.Context, ticket api.F3ParticipationTicket) (api.F3ParticipationLease, error) {
return api.F3ParticipationLease{}, nil
}

func (m *MockF3API) GetCert(ctx context.Context, instance uint64) (*certs.FinalityCertificate, error) {
return nil, nil
}

func (m *MockF3API) SetLatestCert(cert *certs.FinalityCertificate) {
m.lk.Lock()
defer m.lk.Unlock()

m.latestCert = cert
}

func (m *MockF3API) GetLatestCert(ctx context.Context) (*certs.FinalityCertificate, error) {
m.lk.Lock()
defer m.lk.Unlock()

return m.latestCert, nil
}

func (m *MockF3API) GetManifest(ctx context.Context) (*manifest.Manifest, error) {
return nil, nil
}

func (m *MockF3API) GetPowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error) {
return nil, nil
}

func (m *MockF3API) GetF3PowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error) {
return nil, nil
}

func (m *MockF3API) SetEnabled(enabled bool) {
m.lk.Lock()
defer m.lk.Unlock()

m.enabled = enabled
}

func (m *MockF3API) IsEnabled() bool {
m.lk.Lock()
defer m.lk.Unlock()

return m.enabled
}

func (m *MockF3API) SetRunning(running bool) {
m.lk.Lock()
defer m.lk.Unlock()

m.running = running
}

func (m *MockF3API) IsRunning() (bool, error) {
m.lk.Lock()
defer m.lk.Unlock()

return m.running, nil
}

func (m *MockF3API) Progress() (gpbft.Instant, error) {
return gpbft.Instant{}, nil
}

func (m *MockF3API) ListParticipants() ([]api.F3Participant, error) {
return nil, nil
}

var _ lf3.F3API = (*MockF3API)(nil)
162 changes: 162 additions & 0 deletions chain/tsresolver/tipset_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package tsresolver

import (
"context"
"errors"
"fmt"

"github.com/filecoin-project/go-f3/certs"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/actors/policy"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
logging "github.com/ipfs/go-log/v2"
"golang.org/x/xerrors"
)

var log = logging.Logger("chain/tsresolver")

const (
EthBlockSelectorEarliest = "earliest"
EthBlockSelectorPending = "pending"
EthBlockSelectorLatest = "latest"
EthBlockSelectorSafe = "safe"
EthBlockSelectorFinalized = "finalized"
)

type TipSetLoader interface {
GetHeaviestTipSet() (ts *types.TipSet)
LoadTipSet(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error)
GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, anchor *types.TipSet, prev bool) (*types.TipSet, error)
}

type F3 interface {
GetLatestCert(ctx context.Context) (*certs.FinalityCertificate, error)
IsEnabled() bool
IsRunning() (bool, error)
}

type TipSetResolver interface {
ResolveEthBlockSelector(ctx context.Context, selector string, strict bool) (*types.TipSet, error)
}

type tipSetResolver struct {
loader TipSetLoader
f3 F3
}

var _ TipSetResolver = (*tipSetResolver)(nil)

func NewTipSetResolver(loader TipSetLoader, f3 F3) TipSetResolver {
return &tipSetResolver{
loader: loader,
f3: f3,
}
}

// ResolveEthBlockSelector resolves an Ethereum block selector string to a TipSet.
//
// The selector can be one of:
// - "pending": the chain head
// - "latest": the TipSet with the latest executed messages (head - 1)
// - "safe": the TipSet with messages executed at least eth.SafeEpochDelay (30) epochs ago or the
// latest F3 finalized TipSet
// - "finalized": the TipSet with messages executed at least policy.ChainFinality (900) epochs ago
// or the latest F3 finalized TipSet
// - a decimal block number: the TipSet at the given height
// - a 0x-prefixed hex block number: the TipSet at the given height
//
// If a specific block number is specified and `strict` is true, an error is returned if the block
// number resolves to a null round, otherwise in the case of a null round the first non-null TipSet
// immediately before the null round is returned.
func (tsr *tipSetResolver) ResolveEthBlockSelector(ctx context.Context, selector string, strict bool) (*types.TipSet, error) {
switch selector {
case EthBlockSelectorEarliest:
return nil, fmt.Errorf(`block param "%s" is not supported`, EthBlockSelectorEarliest)

case EthBlockSelectorPending:
return tsr.loader.GetHeaviestTipSet(), nil

case EthBlockSelectorLatest:
// head - 1 because we're always one behind for Eth compatibility due to deferred execution
return tsr.loader.LoadTipSet(ctx, tsr.loader.GetHeaviestTipSet().Parents())

case EthBlockSelectorSafe, EthBlockSelectorFinalized:
defaultDelay := policy.ChainFinality
if selector == EthBlockSelectorSafe {
defaultDelay = ethtypes.SafeEpochDelay
}

head := tsr.loader.GetHeaviestTipSet()
latestHeight := head.Height() - 1 // always one behind for Eth compatibility due to deferred execution
defaultHeight := latestHeight - defaultDelay

if f3TipSet, err := tsr.getF3FinalizedTipSet(ctx); err != nil {
return nil, err
} else if f3TipSet != nil && f3TipSet.Height() > defaultHeight {
// return the parent of the finalized tipset (deferred execution, eth cares about t)
return tsr.loader.LoadTipSet(ctx, f3TipSet.Parents())
} // else F3 is disabled, not running, or behind the default safe or finalized height

ts, err := tsr.loader.GetTipsetByHeight(ctx, defaultHeight, head, true)
if err != nil {
return nil, xerrors.Errorf("loading tipset at height %v: %w", defaultHeight, err)
}
return ts, nil

default:
// likely an 0x hex block number or a decimal block number
return tsr.resolveEthBlockNumberSelector(ctx, selector, strict)
}
}

func (tsr *tipSetResolver) getF3FinalizedTipSet(ctx context.Context) (*types.TipSet, error) {
if !tsr.f3.IsEnabled() {
return nil, nil
}
if running, _ := tsr.f3.IsRunning(); !running {
return nil, nil
}

cert, err := tsr.f3.GetLatestCert(ctx)
if err != nil {
log.Debugf("loading latest F3 certificate: %s", err)
return nil, nil
}

tsk, err := types.TipSetKeyFromBytes(cert.ECChain.Head().Key)
if err != nil {
return nil, xerrors.Errorf("decoding tipset key reported by F3: %w", err)
}

finalizedTipSet, err := tsr.loader.LoadTipSet(ctx, tsk)
if err != nil {
return nil, xerrors.Errorf("loading tipset reported as finalized by F3 %s: %w", tsk, err)
}

return finalizedTipSet, nil
}

func (tsr *tipSetResolver) resolveEthBlockNumberSelector(ctx context.Context, selector string, strict bool) (*types.TipSet, error) {
var num ethtypes.EthUint64
if err := num.UnmarshalJSON([]byte(`"` + selector + `"`)); err != nil {
return nil, xerrors.Errorf("cannot parse block number: %v", err)
}

head := tsr.loader.GetHeaviestTipSet()
if abi.ChainEpoch(num) > head.Height()-1 {
return nil, errors.New("requested a future epoch (beyond 'latest')")
}

ts, err := tsr.loader.GetTipsetByHeight(ctx, abi.ChainEpoch(num), head, true)
if err != nil {
return nil, fmt.Errorf("cannot get tipset at height: %v", num)
}

if strict && ts.Height() != abi.ChainEpoch(num) {
return nil, api.NewErrNullRound(abi.ChainEpoch(num))
}

return ts, nil
}
Loading

0 comments on commit cd0f34f

Please sign in to comment.