Skip to content

Commit 4cf4ca2

Browse files
authored
Merge pull request #12861 from filecoin-project/feat/f3-activation-contract
feat(f3): f3-activation-contract integration
2 parents e113aa6 + 7c0c464 commit 4cf4ca2

File tree

12 files changed

+562
-32
lines changed

12 files changed

+562
-32
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
- Exposed `StateGetNetworkParams` in the Lotus Gateway API ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881))
1313
- **BREAKING**: Removed `SupportedProofTypes` from `StateGetNetworkParams` response as it was unreliable and didn't match FVM's actual supported proofs ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881))
1414
- refactor(eth): attach ToFilecoinMessage converter to EthCall method for improved package/module import structure. This change also exports the converter as a public method, enhancing usability for developers utilizing Lotus as a library. ([filecoin-project/lotus#12844](https://github.com/filecoin-project/lotus/pull/12844))
15+
- feat(f3): Implement contract based parameter setting as for FRC-0099 ([filecoin-project/lotus#12861](https://github.com/filecoin-project/lotus/pull/12861))
16+
1517
- chore: switch to pure-go zstd decoder for snapshot imports. ([filecoin-project/lotus#12857](https://github.com/filecoin-project/lotus/pull/12857))
1618
- feat: automatically detect if the genesis is zstd compressed. ([filecoin-project/lotus#12885](https://github.com/filecoin-project/lotus/pull/12885)
1719
- `lotus send` now supports `--csv` option for sending multiple transactions. ([filecoin-project/lotus#12892](https://github.com/filecoin-project/lotus/pull/12892))

chain/lf3/config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lf3
22

33
import (
4+
"os"
45
"time"
56

67
"github.com/ipfs/go-cid"
@@ -34,6 +35,10 @@ type Config struct {
3435
// TESTINGAllowDynamicFinalize allow dynamic manifests to finalize tipsets. DO NOT ENABLE
3536
// THIS IN PRODUCTION!
3637
AllowDynamicFinalize bool
38+
39+
// ContractAddress specifies the address of the contract carring F3 parameters
40+
ContractAddress string
41+
ContractPollInterval time.Duration
3742
}
3843

3944
// NewManifest constructs a sane F3 manifest based on the passed parameters. This function does not
@@ -79,11 +84,22 @@ func NewConfig(nn dtypes.NetworkName) *Config {
7984
if nn == "testnetnet" {
8085
nn = "filecoin"
8186
}
87+
pollInterval := 15 * time.Minute
88+
if envVar := os.Getenv("LOTUS_F3_POLL_INTERVAL"); len(envVar) != 0 {
89+
d, err := time.ParseDuration(envVar)
90+
if err != nil {
91+
log.Errorf("invalid duration in LOTUS_F3_POLL_INTERVAL, defaulting to %v", pollInterval)
92+
} else {
93+
pollInterval = d
94+
}
95+
96+
}
8297
c := &Config{
8398
BaseNetworkName: gpbft.NetworkName(nn),
8499
PrioritizeStaticManifest: true,
85100
DynamicManifestProvider: buildconstants.F3ManifestServerID,
86101
AllowDynamicFinalize: false,
102+
ContractPollInterval: pollInterval,
87103
}
88104
if buildconstants.F3BootstrapEpoch >= 0 {
89105
c.StaticManifest = NewManifest(

chain/lf3/manifest.go

Lines changed: 254 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
package lf3
22

33
import (
4+
"bytes"
5+
"compress/flate"
46
"context"
7+
"encoding/binary"
8+
"encoding/json"
59
"fmt"
10+
"io"
11+
"math"
612
"strings"
13+
"time"
714

815
"github.com/ipfs/go-datastore"
916
"github.com/ipfs/go-datastore/namespace"
1017
pubsub "github.com/libp2p/go-libp2p-pubsub"
18+
"golang.org/x/sync/errgroup"
1119
"golang.org/x/xerrors"
1220

1321
"github.com/filecoin-project/go-f3/ec"
22+
"github.com/filecoin-project/go-f3/gpbft"
1423
"github.com/filecoin-project/go-f3/manifest"
24+
"github.com/filecoin-project/go-state-types/abi"
1525

26+
"github.com/filecoin-project/lotus/api"
1627
"github.com/filecoin-project/lotus/build"
1728
"github.com/filecoin-project/lotus/chain/store"
29+
"github.com/filecoin-project/lotus/chain/types"
30+
"github.com/filecoin-project/lotus/chain/types/ethtypes"
31+
"github.com/filecoin-project/lotus/lib/must"
1832
"github.com/filecoin-project/lotus/node/modules/dtypes"
1933
"github.com/filecoin-project/lotus/node/modules/helpers"
2034
)
@@ -35,12 +49,24 @@ func (hg *headGetter) GetHead(context.Context) (ec.TipSet, error) {
3549
// message topic will be filtered
3650
var MaxDynamicManifestChangesAllowed = 1000
3751

38-
func NewManifestProvider(mctx helpers.MetricsCtx, config *Config, cs *store.ChainStore, ps *pubsub.PubSub, mds dtypes.MetadataDS) (prov manifest.ManifestProvider, err error) {
52+
func NewManifestProvider(mctx helpers.MetricsCtx, config *Config, cs *store.ChainStore, ps *pubsub.PubSub, mds dtypes.MetadataDS, stateCaller StateCaller) (prov manifest.ManifestProvider, err error) {
53+
var primaryManifest manifest.ManifestProvider
54+
if config.StaticManifest != nil {
55+
log.Infof("using static maniest as primary")
56+
primaryManifest, err = manifest.NewStaticManifestProvider(config.StaticManifest)
57+
} else if config.ContractAddress != "" {
58+
log.Infow("using contract maniest as primary", "address", config.ContractAddress)
59+
primaryManifest, err = NewContractManifestProvider(mctx, config, stateCaller)
60+
}
61+
if err != nil {
62+
return nil, fmt.Errorf("creating primary manifest: %w", err)
63+
}
64+
3965
if config.DynamicManifestProvider == "" || !build.IsF3PassiveTestingEnabled() {
40-
if config.StaticManifest == nil {
66+
if config.StaticManifest == nil && config.ContractAddress == "" {
4167
return manifest.NoopManifestProvider{}, nil
4268
}
43-
return manifest.NewStaticManifestProvider(config.StaticManifest)
69+
return primaryManifest, nil
4470
}
4571

4672
opts := []manifest.DynamicManifestProviderOption{
@@ -49,12 +75,6 @@ func NewManifestProvider(mctx helpers.MetricsCtx, config *Config, cs *store.Chai
4975
),
5076
}
5177

52-
if config.StaticManifest != nil {
53-
opts = append(opts,
54-
manifest.DynamicManifestProviderWithInitialManifest(config.StaticManifest),
55-
)
56-
}
57-
5878
if config.AllowDynamicFinalize {
5979
log.Error("dynamic F3 manifests are allowed to finalize tipsets, do not enable this in production!")
6080
}
@@ -84,9 +104,232 @@ func NewManifestProvider(mctx helpers.MetricsCtx, config *Config, cs *store.Chai
84104
if err != nil {
85105
return nil, err
86106
}
87-
if config.PrioritizeStaticManifest && config.StaticManifest != nil {
107+
if config.PrioritizeStaticManifest && primaryManifest != nil {
88108
prov, err = manifest.NewFusingManifestProvider(mctx,
89-
(*headGetter)(cs), prov, config.StaticManifest)
109+
(*headGetter)(cs), prov, primaryManifest)
90110
}
91111
return prov, err
92112
}
113+
114+
type StateCaller interface {
115+
StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (res *api.InvocResult, err error)
116+
}
117+
118+
type ContractManifestProvider struct {
119+
address string
120+
networkName gpbft.NetworkName
121+
stateCaller StateCaller
122+
pollInterval time.Duration
123+
124+
manifestChanges chan *manifest.Manifest
125+
126+
errgrp *errgroup.Group
127+
runningCtx context.Context
128+
cancel context.CancelFunc
129+
}
130+
131+
func NewContractManifestProvider(mctx helpers.MetricsCtx, config *Config, stateCaller StateCaller) (*ContractManifestProvider, error) {
132+
ctx, cancel := context.WithCancel(context.WithoutCancel(mctx))
133+
errgrp, ctx := errgroup.WithContext(ctx)
134+
return &ContractManifestProvider{
135+
stateCaller: stateCaller,
136+
address: config.ContractAddress,
137+
networkName: config.BaseNetworkName,
138+
pollInterval: config.ContractPollInterval,
139+
140+
manifestChanges: make(chan *manifest.Manifest, 1),
141+
142+
errgrp: errgrp,
143+
runningCtx: ctx,
144+
cancel: cancel,
145+
}, nil
146+
}
147+
148+
func (cmp *ContractManifestProvider) Start(context.Context) error {
149+
// no address, nothing to do
150+
if len(cmp.address) == 0 {
151+
// send nil so fusing knows we have nothing
152+
log.Infof("contract manifest provider, address unknown, exiting")
153+
cmp.manifestChanges <- nil
154+
return nil
155+
}
156+
157+
var knownManifest *manifest.Manifest
158+
knownManifest, err := cmp.fetchManifest(cmp.runningCtx)
159+
if err != nil {
160+
log.Warnw("got error while fetching manifest from contract", "error", err)
161+
}
162+
cmp.manifestChanges <- knownManifest
163+
164+
cmp.errgrp.Go(func() error {
165+
t := time.NewTicker(cmp.pollInterval)
166+
defer t.Stop()
167+
168+
loop:
169+
for cmp.runningCtx.Err() == nil {
170+
select {
171+
case <-t.C:
172+
m, err := cmp.fetchManifest(cmp.runningCtx)
173+
if err != nil {
174+
log.Warnw("got error while fetching manifest from contract", "error", err)
175+
continue loop
176+
}
177+
178+
if knownManifest.Equal(m) {
179+
continue loop
180+
}
181+
182+
c, err := m.Cid()
183+
if err != nil {
184+
log.Errorf("got error while computing manifest CID")
185+
}
186+
187+
if m != nil {
188+
log.Infow("new manifest from contract", "enabled", true,
189+
"bootstrapEpoch", m.BootstrapEpoch,
190+
"manifestCID", c)
191+
} else {
192+
log.Info("new manifest from contract", "enabled", false)
193+
}
194+
cmp.manifestChanges <- m
195+
knownManifest = m
196+
case <-cmp.runningCtx.Done():
197+
}
198+
}
199+
200+
return nil
201+
})
202+
return nil
203+
}
204+
205+
func decompressManifest(compressedManifest []byte) (*manifest.Manifest, error) {
206+
reader := io.LimitReader(flate.NewReader(bytes.NewReader(compressedManifest)), 1<<20)
207+
var m manifest.Manifest
208+
err := json.NewDecoder(reader).Decode(&m)
209+
if err != nil {
210+
return nil, err
211+
}
212+
return &m, nil
213+
}
214+
215+
func (cmp *ContractManifestProvider) fetchManifest(ctx context.Context) (*manifest.Manifest, error) {
216+
ethReturn, err := cmp.callContract(ctx)
217+
if err != nil {
218+
return nil, fmt.Errorf("calling contract at %s: %w", cmp.address, err)
219+
}
220+
if len(ethReturn) == 0 {
221+
return nil, nil
222+
}
223+
224+
activationEpoch, compressedManifest, err := parseContractReturn(ethReturn)
225+
if err != nil {
226+
return nil, fmt.Errorf("parsing contract information: %w", err)
227+
}
228+
229+
if activationEpoch == math.MaxUint64 || len(compressedManifest) == 0 {
230+
return nil, nil
231+
}
232+
233+
m, err := decompressManifest(compressedManifest)
234+
if err != nil {
235+
return nil, fmt.Errorf("got error while decoding manifest: %w", err)
236+
}
237+
238+
if m.BootstrapEpoch < 0 || uint64(m.BootstrapEpoch) != activationEpoch {
239+
return nil, fmt.Errorf("bootstrap epoch does not match: %d != %d", m.BootstrapEpoch, activationEpoch)
240+
}
241+
242+
if err := m.Validate(); err != nil {
243+
return nil, fmt.Errorf("manifest does not validate: %w", err)
244+
}
245+
246+
if m.NetworkName != cmp.networkName {
247+
return nil, fmt.Errorf("network name does not match, expected: %s, got: %s",
248+
cmp.networkName, m.NetworkName)
249+
}
250+
251+
return m, nil
252+
}
253+
254+
func parseContractReturn(retBytes []byte) (uint64, []byte, error) {
255+
// 3*32 because there should be 3 slots minimum
256+
if len(retBytes) < 3*32 {
257+
return 0, nil, fmt.Errorf("no activation information")
258+
}
259+
260+
var slot []byte
261+
// split off first slot
262+
slot, retBytes = retBytes[:32], retBytes[32:]
263+
// it is uint64 so we want the last 8 bytes
264+
slot = slot[24:32]
265+
activationEpoch := binary.BigEndian.Uint64(slot)
266+
267+
// next slot is the offest to variable length bytes
268+
// it is always the same 0x00000...0040
269+
slot, retBytes = retBytes[:32], retBytes[32:]
270+
for i := 0; i < 31; i++ {
271+
if slot[i] != 0 {
272+
return 0, nil, fmt.Errorf("wrong value for offest (padding): slot[%d] = 0x%x != 0x00", i, slot[i])
273+
}
274+
}
275+
if slot[31] != 0x40 {
276+
return 0, nil, fmt.Errorf("wrong value for offest : slot[31] = 0x%x != 0x40", slot[31])
277+
}
278+
279+
// finally after that there are manifest bytes
280+
// starts with length in a full slot, slot no 3
281+
slot, retBytes = retBytes[:32], retBytes[32:]
282+
slot = slot[24:32]
283+
pLen := binary.BigEndian.Uint64(slot)
284+
if pLen > 4<<10 {
285+
return 0, nil, fmt.Errorf("too long declared payload: %d > %d", pLen, 4<<10)
286+
}
287+
payloadLength := int(pLen)
288+
289+
if payloadLength > len(retBytes) {
290+
return 0, nil, fmt.Errorf("not enough remaining bytes: %d > %d", payloadLength, retBytes)
291+
}
292+
293+
return activationEpoch, retBytes[:payloadLength], nil
294+
}
295+
296+
func (cmp *ContractManifestProvider) callContract(ctx context.Context) ([]byte, error) {
297+
address, err := ethtypes.ParseEthAddress(cmp.address)
298+
if err != nil {
299+
return nil, fmt.Errorf("trying to parse contract address: %s: %w", cmp.address, err)
300+
}
301+
302+
ethCall := ethtypes.EthCall{
303+
To: &address,
304+
Data: must.One(ethtypes.DecodeHexString("0x2587660d")), // method ID of activationInformation()
305+
}
306+
307+
fMessage, err := ethCall.ToFilecoinMessage()
308+
if err != nil {
309+
return nil, fmt.Errorf("converting to filecoin message: %w", err)
310+
}
311+
312+
msgRes, err := cmp.stateCaller.StateCall(ctx, fMessage, types.EmptyTSK)
313+
if err != nil {
314+
return nil, fmt.Errorf("state call error: %w", err)
315+
}
316+
if msgRes.MsgRct.ExitCode != 0 {
317+
return nil, fmt.Errorf("message returned exit code %v: %v", msgRes.MsgRct.ExitCode, msgRes.Error)
318+
}
319+
320+
var ethReturn abi.CborBytes
321+
err = ethReturn.UnmarshalCBOR(bytes.NewReader(msgRes.MsgRct.Return))
322+
if err != nil {
323+
return nil, fmt.Errorf("could not decode return value: %w", err)
324+
}
325+
return []byte(ethReturn), nil
326+
}
327+
328+
func (cmp *ContractManifestProvider) Stop(context.Context) error {
329+
cmp.cancel()
330+
return cmp.errgrp.Wait()
331+
}
332+
333+
func (cmp *ContractManifestProvider) ManifestUpdates() <-chan *manifest.Manifest {
334+
return cmp.manifestChanges
335+
}

0 commit comments

Comments
 (0)