Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: f3 activation contract #6447

Merged
merged 1 commit into from
Feb 25, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/submodule/f3/f3_submodule.go
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ func NewF3Submodule(ctx context.Context,
return nil, err
}

provider, err := vf3.NewManifestProvider(ctx, network.F3Cfg, chain.ChainReader, network.Pubsub, repo.MetaDatastore())
provider, err := vf3.NewManifestProvider(ctx, network.F3Cfg, chain.ChainReader, network.Pubsub, repo.MetaDatastore(), chain.API())
if err != nil {
return nil, err
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ require (
github.com/filecoin-project/go-commp-utils v0.1.3
github.com/filecoin-project/go-crypto v0.1.0
github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc6
github.com/filecoin-project/go-f3 v0.8.0
github.com/filecoin-project/go-f3 v0.8.1
github.com/filecoin-project/go-fil-commcid v0.2.0
github.com/filecoin-project/go-fil-markets v1.28.2
github.com/filecoin-project/go-jsonrpc v0.1.5
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1702,8 +1702,8 @@ github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc6 h1:EsbXTWsBKT764qtX4M
github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc6/go.mod h1:cX1acvFVWC5EXnnmFPWEFXbO7nLUdSZa+nqgi1QpTpw=
github.com/filecoin-project/go-ds-versioning v0.1.2 h1:to4pTadv3IeV1wvgbCbN6Vqd+fu+7tveXgv/rCEZy6w=
github.com/filecoin-project/go-ds-versioning v0.1.2/go.mod h1:C9/l9PnB1+mwPa26BBVpCjG/XQCB0yj/q5CK2J8X1I4=
github.com/filecoin-project/go-f3 v0.8.0 h1:Zm2NIhryHFudh3QZ5X0qXAqGm/rMc9zjndGLd4vL07A=
github.com/filecoin-project/go-f3 v0.8.0/go.mod h1:zNFGuBM+fYuGXk2fpzl6wW4g2Gyrxgg6z2IVSoGt+60=
github.com/filecoin-project/go-f3 v0.8.1 h1:stlc3ZW5rHXVdvTbCreKh1475GB7noTtvfPGraOkdRA=
github.com/filecoin-project/go-f3 v0.8.1/go.mod h1:dAqNQ59L/zxjt32KI5kM8gzPtN8odwYHTjNcEq5wp1s=
github.com/filecoin-project/go-fil-commcid v0.0.0-20201016201715-d41df56b4f6a/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ=
github.com/filecoin-project/go-fil-commcid v0.1.0/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ=
github.com/filecoin-project/go-fil-commcid v0.2.0 h1:B+5UX8XGgdg/XsdUpST4pEBviKkFOw+Fvl2bLhSKGpI=
16 changes: 16 additions & 0 deletions pkg/vf3/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package vf3

import (
"os"
"time"

"github.com/ipfs/go-cid"
@@ -32,6 +33,10 @@ type Config struct {
// TESTINGAllowDynamicFinalize allow dynamic manifests to finalize tipsets. DO NOT ENABLE
// THIS IN PRODUCTION!
AllowDynamicFinalize bool

// ContractAddress specifies the address of the contract carring F3 parameters
ContractAddress string
ContractPollInterval time.Duration
}

// NewManifest constructs a sane F3 manifest based on the passed parameters. This function does not
@@ -81,11 +86,22 @@ func NewConfig(nn string, netCfg *config.NetworkParamsConfig) (*Config, error) {
if err != nil {
return nil, err
}
pollInterval := 15 * time.Minute
if envVar := os.Getenv("VENUS_F3_POLL_INTERVAL"); len(envVar) != 0 {
d, err := time.ParseDuration(envVar)
if err != nil {
log.Errorf("invalid duration in VENUS_F3_POLL_INTERVAL, defaulting to %v", pollInterval)
} else {
pollInterval = d
}

}
c := &Config{
BaseNetworkName: gpbft.NetworkName(nn),
PrioritizeStaticManifest: true,
DynamicManifestProvider: manifestServerID,
AllowDynamicFinalize: false,
ContractPollInterval: pollInterval,
}
if netCfg.F3BootstrapEpoch >= 0 {
// todo:
269 changes: 258 additions & 11 deletions pkg/vf3/manifest.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package vf3

import (
"bytes"
"compress/flate"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"math"
"strings"
"time"

"github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/namespace"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"golang.org/x/sync/errgroup"

"github.com/filecoin-project/go-f3/ec"
"github.com/filecoin-project/go-f3/gpbft"
"github.com/filecoin-project/go-f3/manifest"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/venus/pkg/chain"
"github.com/filecoin-project/venus/venus-shared/types"
must "github.com/filecoin-project/venus/venus-shared/utils"
)

type headGetter struct {
@@ -32,12 +44,30 @@ func (hg *headGetter) GetHead(_ context.Context) (ec.TipSet, error) {
// message topic will be filtered
var MaxDynamicManifestChangesAllowed = 1000

func NewManifestProvider(ctx context.Context, config *Config, cs *chain.Store, ps *pubsub.PubSub, mds datastore.Datastore) (prov manifest.ManifestProvider, err error) {
func NewManifestProvider(ctx context.Context,
config *Config,
cs *chain.Store,
ps *pubsub.PubSub,
mds datastore.Datastore,
stateCaller StateCaller,
) (prov manifest.ManifestProvider, err error) {
var primaryManifest manifest.ManifestProvider
if config.StaticManifest != nil {
log.Infof("using static maniest as primary")
primaryManifest, err = manifest.NewStaticManifestProvider(config.StaticManifest)
} else if config.ContractAddress != "" {
log.Infow("using contract maniest as primary", "address", config.ContractAddress)
primaryManifest, err = NewContractManifestProvider(ctx, config, stateCaller)
}
if err != nil {
return nil, fmt.Errorf("creating primary manifest: %w", err)
}

if config.DynamicManifestProvider == "" {
if config.StaticManifest == nil {
if config.StaticManifest == nil && config.ContractAddress == "" {
return manifest.NoopManifestProvider{}, nil
}
return manifest.NewStaticManifestProvider(config.StaticManifest)
return primaryManifest, nil
}

opts := []manifest.DynamicManifestProviderOption{
@@ -46,12 +76,6 @@ func NewManifestProvider(ctx context.Context, config *Config, cs *chain.Store, p
),
}

if config.StaticManifest != nil {
opts = append(opts,
manifest.DynamicManifestProviderWithInitialManifest(config.StaticManifest),
)
}

if config.AllowDynamicFinalize {
log.Error("dynamic F3 manifests are allowed to finalize tipsets, do not enable this in production!")
}
@@ -81,10 +105,233 @@ func NewManifestProvider(ctx context.Context, config *Config, cs *chain.Store, p
if err != nil {
return nil, err
}
if config.PrioritizeStaticManifest && config.StaticManifest != nil {
if config.PrioritizeStaticManifest && primaryManifest != nil {
prov, err = manifest.NewFusingManifestProvider(ctx,
&headGetter{cs}, prov, config.StaticManifest)
&headGetter{cs}, prov, primaryManifest)
}

return prov, err
}

type StateCaller interface {
StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (res *types.InvocResult, err error)
}

type ContractManifestProvider struct {
address string
networkName gpbft.NetworkName
stateCaller StateCaller
pollInterval time.Duration

manifestChanges chan *manifest.Manifest

errgrp *errgroup.Group
runningCtx context.Context
cancel context.CancelFunc
}

func NewContractManifestProvider(ctx context.Context, config *Config, stateCaller StateCaller) (*ContractManifestProvider, error) {
ctx, cancel := context.WithCancel(context.WithoutCancel(ctx))
errgrp, ctx := errgroup.WithContext(ctx)
return &ContractManifestProvider{
stateCaller: stateCaller,
address: config.ContractAddress,
networkName: config.BaseNetworkName,
pollInterval: config.ContractPollInterval,

manifestChanges: make(chan *manifest.Manifest, 1),

errgrp: errgrp,
runningCtx: ctx,
cancel: cancel,
}, nil
}

func (cmp *ContractManifestProvider) Start(context.Context) error {
// no address, nothing to do
if len(cmp.address) == 0 {
// send nil so fusing knows we have nothing
log.Infof("contract manifest provider, address unknown, exiting")
cmp.manifestChanges <- nil
return nil
}

var knownManifest *manifest.Manifest
knownManifest, err := cmp.fetchManifest(cmp.runningCtx)
if err != nil {
log.Warnw("got error while fetching manifest from contract", "error", err)
}
cmp.manifestChanges <- knownManifest

cmp.errgrp.Go(func() error {
t := time.NewTicker(cmp.pollInterval)
defer t.Stop()

loop:
for cmp.runningCtx.Err() == nil {
select {
case <-t.C:
m, err := cmp.fetchManifest(cmp.runningCtx)
if err != nil {
log.Warnw("got error while fetching manifest from contract", "error", err)
continue loop
}

if knownManifest.Equal(m) {
continue loop
}

c, err := m.Cid()
if err != nil {
log.Errorf("got error while computing manifest CID")
}

if m != nil {
log.Infow("new manifest from contract", "enabled", true,
"bootstrapEpoch", m.BootstrapEpoch,
"manifestCID", c)
} else {
log.Info("new manifest from contract", "enabled", false)
}
cmp.manifestChanges <- m
knownManifest = m
case <-cmp.runningCtx.Done():
}
}

return nil
})
return nil
}

func decompressManifest(compressedManifest []byte) (*manifest.Manifest, error) {
reader := io.LimitReader(flate.NewReader(bytes.NewReader(compressedManifest)), 1<<20)
var m manifest.Manifest
err := json.NewDecoder(reader).Decode(&m)
if err != nil {
return nil, err
}
return &m, nil
}

func (cmp *ContractManifestProvider) fetchManifest(ctx context.Context) (*manifest.Manifest, error) {
ethReturn, err := cmp.callContract(ctx)
if err != nil {
return nil, fmt.Errorf("calling contract at %s: %w", cmp.address, err)
}
if len(ethReturn) == 0 {
return nil, nil
}

activationEpoch, compressedManifest, err := parseContractReturn(ethReturn)
if err != nil {
return nil, fmt.Errorf("parsing contract information: %w", err)
}

if activationEpoch == math.MaxUint64 || len(compressedManifest) == 0 {
return nil, nil
}

m, err := decompressManifest(compressedManifest)
if err != nil {
return nil, fmt.Errorf("got error while decoding manifest: %w", err)
}

if m.BootstrapEpoch < 0 || uint64(m.BootstrapEpoch) != activationEpoch {
return nil, fmt.Errorf("bootstrap epoch does not match: %d != %d", m.BootstrapEpoch, activationEpoch)
}

if err := m.Validate(); err != nil {
return nil, fmt.Errorf("manifest does not validate: %w", err)
}

if m.NetworkName != cmp.networkName {
return nil, fmt.Errorf("network name does not match, expected: %s, got: %s",
cmp.networkName, m.NetworkName)
}

return m, nil
}

func parseContractReturn(retBytes []byte) (uint64, []byte, error) {
// 3*32 because there should be 3 slots minimum
if len(retBytes) < 3*32 {
return 0, nil, fmt.Errorf("no activation information")
}

var slot []byte
// split off first slot
slot, retBytes = retBytes[:32], retBytes[32:]
// it is uint64 so we want the last 8 bytes
slot = slot[24:32]
activationEpoch := binary.BigEndian.Uint64(slot)

// next slot is the offest to variable length bytes
// it is always the same 0x00000...0040
slot, retBytes = retBytes[:32], retBytes[32:]
for i := 0; i < 31; i++ {
if slot[i] != 0 {
return 0, nil, fmt.Errorf("wrong value for offest (padding): slot[%d] = 0x%x != 0x00", i, slot[i])
}
}
if slot[31] != 0x40 {
return 0, nil, fmt.Errorf("wrong value for offest : slot[31] = 0x%x != 0x40", slot[31])
}

// finally after that there are manifest bytes
// starts with length in a full slot, slot no 3
slot, retBytes = retBytes[:32], retBytes[32:]
slot = slot[24:32]
pLen := binary.BigEndian.Uint64(slot)
if pLen > 4<<10 {
return 0, nil, fmt.Errorf("too long declared payload: %d > %d", pLen, 4<<10)
}
payloadLength := int(pLen)

if payloadLength > len(retBytes) {
return 0, nil, fmt.Errorf("not enough remaining bytes: %d > %d", payloadLength, retBytes)
}

return activationEpoch, retBytes[:payloadLength], nil
}

func (cmp *ContractManifestProvider) callContract(ctx context.Context) ([]byte, error) {
address, err := types.ParseEthAddress(cmp.address)
if err != nil {
return nil, fmt.Errorf("trying to parse contract address: %s: %w", cmp.address, err)
}

ethCall := types.EthCall{
To: &address,
Data: must.One(types.DecodeHexString("0x2587660d")), // method ID of activationInformation()
}

fMessage, err := ethCall.ToFilecoinMessage()
if err != nil {
return nil, fmt.Errorf("converting to filecoin message: %w", err)
}

msgRes, err := cmp.stateCaller.StateCall(ctx, fMessage, types.EmptyTSK)
if err != nil {
return nil, fmt.Errorf("state call error: %w", err)
}
if msgRes.MsgRct.ExitCode != 0 {
return nil, fmt.Errorf("message returned exit code %v: %v", msgRes.MsgRct.ExitCode, msgRes.Error)
}

var ethReturn abi.CborBytes
err = ethReturn.UnmarshalCBOR(bytes.NewReader(msgRes.MsgRct.Return))
if err != nil {
return nil, fmt.Errorf("could not decode return value: %w", err)
}
return []byte(ethReturn), nil
}

func (cmp *ContractManifestProvider) Stop(context.Context) error {
cmp.cancel()
return cmp.errgrp.Wait()
}

func (cmp *ContractManifestProvider) ManifestUpdates() <-chan *manifest.Manifest {
return cmp.manifestChanges
}
Loading