From b5c951a06985abe5e5672ceaf63d112d512821bb Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 3 Sep 2024 22:09:05 +1000 Subject: [PATCH] fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs Fixes: https://github.com/filecoin-project/lotus/issues/12414 Previously StateGetBeaconEntry would always try and use a drand beacon to get the appropriate round. But as drand has shut down old beacons and we've removed client details from Lotus, it has stopped working for historical beacons. This fix restores historical beacon entries by using the on-chain lookup, however it now follows the rules used by StateGetRandomnessFromBeacon and the get_beacon_randomness syscall which has some quirks with null rounds prior to nv14. See https://github.com/filecoin-project/lotus/issues/12414#issuecomment-2320034935 for specifics. StateGetBeaconEntry still blocks for future epochs and uses live drand beacon clients to wait for and fetch rounds as they are available. --- CHANGELOG.md | 1 + api/api_full.go | 7 +- build/openrpc/full.json | 2 +- chain/beacon/mock.go | 75 +++++- chain/gen/genesis/miners.go | 5 + chain/rand/rand.go | 107 ++++---- chain/stmgr/stmgr.go | 11 +- conformance/rand_fixed.go | 5 + conformance/rand_record.go | 25 +- conformance/rand_replay.go | 19 +- documentation/en/api-v1-unstable-methods.md | 7 +- node/impl/full/state.go | 10 + node/impl/full/state_test.go | 282 ++++++++++++++++++++ 13 files changed, 471 insertions(+), 85 deletions(-) create mode 100644 node/impl/full/state_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 669129d5af5..a1d3b21f2d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ## Improvements - Reduce size of embedded genesis CAR files by removing WASM actor blocks and compressing with zstd. This reduces the `lotus` binary size by approximately 10 MiB. ([filecoin-project/lotus#12439](https://github.com/filecoin-project/lotus/pull/12439)) +- Legacy/historical Drand lookups via `StateGetBeaconEntry` now work again for all historical epochs. `StateGetBeaconEntry` now uses the on-chain beacon entries and follows the same rules for historical Drand round matching as `StateGetRandomnessFromBeacon` and the `get_beacon_randomness` FVM syscall. Be aware that there will be some some variance in matching Filecoin epochs to Drand rounds where null Filecoin rounds are involved prior to network version 14. ([filecoin-project/lotus#12428](https://github.com/filecoin-project/lotus/pull/12428)). # Node v1.29.0 / 2024-09-02 diff --git a/api/api_full.go b/api/api_full.go index 271aa2dd43b..259060538b0 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -580,9 +580,10 @@ type FullNode interface { // StateGetRandomnessDigestFromBeacon is used to sample the beacon for randomness. StateGetRandomnessDigestFromBeacon(ctx context.Context, randEpoch abi.ChainEpoch, tsk types.TipSetKey) (abi.Randomness, error) //perm:read - // StateGetBeaconEntry returns the beacon entry for the given filecoin epoch. If - // the entry has not yet been produced, the call will block until the entry - // becomes available + // StateGetBeaconEntry returns the beacon entry for the given filecoin epoch + // by using the recorded entries on the chain. If the entry for the requested + // epoch has not yet been produced, the call will block until the entry + // becomes available. StateGetBeaconEntry(ctx context.Context, epoch abi.ChainEpoch) (*types.BeaconEntry, error) //perm:read // StateGetNetworkParams return current network params diff --git a/build/openrpc/full.json b/build/openrpc/full.json index 862211eb411..525ce2dd78d 100644 --- a/build/openrpc/full.json +++ b/build/openrpc/full.json @@ -18477,7 +18477,7 @@ { "name": "Filecoin.StateGetBeaconEntry", "description": "```go\nfunc (s *FullNodeStruct) StateGetBeaconEntry(p0 context.Context, p1 abi.ChainEpoch) (*types.BeaconEntry, error) {\n\tif s.Internal.StateGetBeaconEntry == nil {\n\t\treturn nil, ErrNotSupported\n\t}\n\treturn s.Internal.StateGetBeaconEntry(p0, p1)\n}\n```", - "summary": "StateGetBeaconEntry returns the beacon entry for the given filecoin epoch. If\nthe entry has not yet been produced, the call will block until the entry\nbecomes available\n", + "summary": "StateGetBeaconEntry returns the beacon entry for the given filecoin epoch\nby using the recorded entries on the chain. If the entry for the requested\nepoch has not yet been produced, the call will block until the entry\nbecomes available.\n", "paramStructure": "by-position", "params": [ { diff --git a/chain/beacon/mock.go b/chain/beacon/mock.go index 7f0effa005c..166fa133c5b 100644 --- a/chain/beacon/mock.go +++ b/chain/beacon/mock.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/binary" + "sync" "time" "golang.org/x/crypto/blake2b" @@ -15,26 +16,54 @@ import ( "github.com/filecoin-project/lotus/chain/types" ) -// mockBeacon assumes that filecoin rounds are 1:1 mapped with the beacon rounds -type mockBeacon struct { - interval time.Duration +// MockBeacon assumes that filecoin rounds are 1:1 mapped with the beacon rounds +type MockBeacon struct { + interval time.Duration + maxIndex int + waitingEntry int + lk sync.Mutex + cond *sync.Cond } -func (mb *mockBeacon) IsChained() bool { +func (mb *MockBeacon) IsChained() bool { return true } func NewMockBeacon(interval time.Duration) RandomBeacon { - mb := &mockBeacon{interval: interval} - + mb := &MockBeacon{interval: interval, maxIndex: -1} + mb.cond = sync.NewCond(&mb.lk) return mb } -func (mb *mockBeacon) RoundTime() time.Duration { +// SetMaxIndex sets the maximum index that the beacon will return, and optionally blocks until all +// waiting requests are satisfied. If maxIndex is -1, the beacon will return entries indefinitely. +func (mb *MockBeacon) SetMaxIndex(maxIndex int, blockTillNoneWaiting bool) { + mb.lk.Lock() + defer mb.lk.Unlock() + mb.maxIndex = maxIndex + mb.cond.Broadcast() + if !blockTillNoneWaiting { + return + } + + for mb.waitingEntry > 0 { + mb.cond.Wait() + } +} + +// WaitingOnEntryCount returns the number of requests that are currently waiting for an entry. Where +// maxIndex has not been set, this will always return 0 as beacon entries are generated on demand. +func (mb *MockBeacon) WaitingOnEntryCount() int { + mb.lk.Lock() + defer mb.lk.Unlock() + return mb.waitingEntry +} + +func (mb *MockBeacon) RoundTime() time.Duration { return mb.interval } -func (mb *mockBeacon) entryForIndex(index uint64) types.BeaconEntry { +func (mb *MockBeacon) entryForIndex(index uint64) types.BeaconEntry { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, index) rval := blake2b.Sum256(buf) @@ -44,14 +73,32 @@ func (mb *mockBeacon) entryForIndex(index uint64) types.BeaconEntry { } } -func (mb *mockBeacon) Entry(ctx context.Context, index uint64) <-chan Response { - e := mb.entryForIndex(index) +func (mb *MockBeacon) Entry(ctx context.Context, index uint64) <-chan Response { out := make(chan Response, 1) - out <- Response{Entry: e} + + mb.lk.Lock() + defer mb.lk.Unlock() + + if mb.maxIndex >= 0 && index > uint64(mb.maxIndex) { + mb.waitingEntry++ + go func() { + mb.lk.Lock() + defer mb.lk.Unlock() + for index > uint64(mb.maxIndex) { + mb.cond.Wait() + } + out <- Response{Entry: mb.entryForIndex(index)} + mb.waitingEntry-- + mb.cond.Broadcast() + }() + } else { + out <- Response{Entry: mb.entryForIndex(index)} + } + return out } -func (mb *mockBeacon) VerifyEntry(from types.BeaconEntry, _prevEntrySig []byte) error { +func (mb *MockBeacon) VerifyEntry(from types.BeaconEntry, _prevEntrySig []byte) error { // TODO: cache this, especially for bls oe := mb.entryForIndex(from.Round) if !bytes.Equal(from.Data, oe.Data) { @@ -60,9 +107,9 @@ func (mb *mockBeacon) VerifyEntry(from types.BeaconEntry, _prevEntrySig []byte) return nil } -func (mb *mockBeacon) MaxBeaconRoundForEpoch(nv network.Version, epoch abi.ChainEpoch) uint64 { +func (mb *MockBeacon) MaxBeaconRoundForEpoch(nv network.Version, epoch abi.ChainEpoch) uint64 { // offset for better testing return uint64(epoch + 100) } -var _ RandomBeacon = (*mockBeacon)(nil) +var _ RandomBeacon = (*MockBeacon)(nil) diff --git a/chain/gen/genesis/miners.go b/chain/gen/genesis/miners.go index 2d55a9ef0b6..02b4f0f83cb 100644 --- a/chain/gen/genesis/miners.go +++ b/chain/gen/genesis/miners.go @@ -647,6 +647,11 @@ func (fr *fakeRand) GetChainRandomness(ctx context.Context, randEpoch abi.ChainE return *(*[32]byte)(out), nil } +func (fr *fakeRand) GetBeaconEntry(ctx context.Context, randEpoch abi.ChainEpoch) (*types.BeaconEntry, error) { + r, _ := fr.GetChainRandomness(ctx, randEpoch) + return &types.BeaconEntry{Round: 10, Data: r[:]}, nil +} + func (fr *fakeRand) GetBeaconRandomness(ctx context.Context, randEpoch abi.ChainEpoch) ([32]byte, error) { out := make([]byte, 32) _, _ = rand.New(rand.NewSource(int64(randEpoch))).Read(out) //nolint diff --git a/chain/rand/rand.go b/chain/rand/rand.go index ff995597e79..8d569321699 100644 --- a/chain/rand/rand.go +++ b/chain/rand/rand.go @@ -111,6 +111,7 @@ type stateRand struct { type Rand interface { GetChainRandomness(ctx context.Context, round abi.ChainEpoch) ([32]byte, error) + GetBeaconEntry(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) GetBeaconRandomness(ctx context.Context, round abi.ChainEpoch) ([32]byte, error) } @@ -124,48 +125,51 @@ func NewStateRand(cs *store.ChainStore, blks []cid.Cid, b beacon.Schedule, netwo } // network v0-12 -func (sr *stateRand) getBeaconRandomnessV1(ctx context.Context, round abi.ChainEpoch) ([32]byte, error) { +func (sr *stateRand) getBeaconEntryV1(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) { randTs, err := sr.GetBeaconRandomnessTipset(ctx, round, true) if err != nil { - return [32]byte{}, err - } - - be, err := sr.cs.GetLatestBeaconEntry(ctx, randTs) - if err != nil { - return [32]byte{}, err + return nil, err } - - return blake2b.Sum256(be.Data), nil + return sr.cs.GetLatestBeaconEntry(ctx, randTs) } // network v13 -func (sr *stateRand) getBeaconRandomnessV2(ctx context.Context, round abi.ChainEpoch) ([32]byte, error) { +func (sr *stateRand) getBeaconEntryV2(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) { randTs, err := sr.GetBeaconRandomnessTipset(ctx, round, false) if err != nil { - return [32]byte{}, err + return nil, err } + return sr.cs.GetLatestBeaconEntry(ctx, randTs) +} - be, err := sr.cs.GetLatestBeaconEntry(ctx, randTs) +// network v14 and on +func (sr *stateRand) getBeaconEntryV3(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) { + randTs, err := sr.GetBeaconRandomnessTipset(ctx, filecoinEpoch, false) if err != nil { - return [32]byte{}, err + return nil, err } - return blake2b.Sum256(be.Data), nil -} + nv := sr.networkVersionGetter(ctx, filecoinEpoch) -// network v14 and on -func (sr *stateRand) getBeaconRandomnessV3(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) { - if filecoinEpoch < 0 { - return sr.getBeaconRandomnessV2(ctx, filecoinEpoch) - } + round := sr.beacon.BeaconForEpoch(filecoinEpoch).MaxBeaconRoundForEpoch(nv, filecoinEpoch) - be, err := sr.extractBeaconEntryForEpoch(ctx, filecoinEpoch) - if err != nil { - log.Errorf("failed to get beacon entry as expected: %s", err) - return [32]byte{}, err + for i := 0; i < 20; i++ { + cbe := randTs.Blocks()[0].BeaconEntries + for _, v := range cbe { + if v.Round == round { + return &v, nil + } + } + + next, err := sr.cs.LoadTipSet(ctx, randTs.Parents()) + if err != nil { + return nil, xerrors.Errorf("failed to load parents when searching back for beacon entry: %w", err) + } + + randTs = next } - return blake2b.Sum256(be.Data), nil + return nil, xerrors.Errorf("didn't find beacon for round %d (epoch %d)", round, filecoinEpoch) } func (sr *stateRand) GetChainRandomness(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) { @@ -178,15 +182,27 @@ func (sr *stateRand) GetChainRandomness(ctx context.Context, filecoinEpoch abi.C return sr.getChainRandomness(ctx, filecoinEpoch, true) } -func (sr *stateRand) GetBeaconRandomness(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) { +func (sr *stateRand) GetBeaconEntry(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) { nv := sr.networkVersionGetter(ctx, filecoinEpoch) - if nv >= network.Version14 { - return sr.getBeaconRandomnessV3(ctx, filecoinEpoch) - } else if nv == network.Version13 { - return sr.getBeaconRandomnessV2(ctx, filecoinEpoch) + if filecoinEpoch > 0 && nv >= network.Version14 { + be, err := sr.getBeaconEntryV3(ctx, filecoinEpoch) + if err != nil { + log.Errorf("failed to get beacon entry as expected: %s", err) + } + return be, err + } else if nv == network.Version13 || filecoinEpoch < 0 { + return sr.getBeaconEntryV2(ctx, filecoinEpoch) + } + return sr.getBeaconEntryV1(ctx, filecoinEpoch) +} + +func (sr *stateRand) GetBeaconRandomness(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) { + be, err := sr.GetBeaconEntry(ctx, filecoinEpoch) + if err != nil { + return [32]byte{}, err } - return sr.getBeaconRandomnessV1(ctx, filecoinEpoch) + return blake2b.Sum256(be.Data), nil } func (sr *stateRand) DrawChainRandomness(ctx context.Context, pers crypto.DomainSeparationTag, filecoinEpoch abi.ChainEpoch, entropy []byte) ([]byte, error) { @@ -218,32 +234,3 @@ func (sr *stateRand) DrawBeaconRandomness(ctx context.Context, pers crypto.Domai return ret, nil } - -func (sr *stateRand) extractBeaconEntryForEpoch(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) { - randTs, err := sr.GetBeaconRandomnessTipset(ctx, filecoinEpoch, false) - if err != nil { - return nil, err - } - - nv := sr.networkVersionGetter(ctx, filecoinEpoch) - - round := sr.beacon.BeaconForEpoch(filecoinEpoch).MaxBeaconRoundForEpoch(nv, filecoinEpoch) - - for i := 0; i < 20; i++ { - cbe := randTs.Blocks()[0].BeaconEntries - for _, v := range cbe { - if v.Round == round { - return &v, nil - } - } - - next, err := sr.cs.LoadTipSet(ctx, randTs.Parents()) - if err != nil { - return nil, xerrors.Errorf("failed to load parents when searching back for beacon entry: %w", err) - } - - randTs = next - } - - return nil, xerrors.Errorf("didn't find beacon for round %d (epoch %d)", round, filecoinEpoch) -} diff --git a/chain/stmgr/stmgr.go b/chain/stmgr/stmgr.go index 2e29dc8e746..49be6fdaec4 100644 --- a/chain/stmgr/stmgr.go +++ b/chain/stmgr/stmgr.go @@ -572,9 +572,17 @@ func (sm *StateManager) GetRandomnessDigestFromBeacon(ctx context.Context, randE } r := rand.NewStateRand(sm.ChainStore(), pts.Cids(), sm.beacon, sm.GetNetworkVersion) - return r.GetBeaconRandomness(ctx, randEpoch) +} +func (sm *StateManager) GetBeaconEntry(ctx context.Context, randEpoch abi.ChainEpoch, tsk types.TipSetKey) (*types.BeaconEntry, error) { + pts, err := sm.ChainStore().GetTipSetFromKey(ctx, tsk) + if err != nil { + return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) + } + + r := rand.NewStateRand(sm.ChainStore(), pts.Cids(), sm.beacon, sm.GetNetworkVersion) + return r.GetBeaconEntry(ctx, randEpoch) } func (sm *StateManager) GetRandomnessDigestFromTickets(ctx context.Context, randEpoch abi.ChainEpoch, tsk types.TipSetKey) ([32]byte, error) { @@ -584,6 +592,5 @@ func (sm *StateManager) GetRandomnessDigestFromTickets(ctx context.Context, rand } r := rand.NewStateRand(sm.ChainStore(), pts.Cids(), sm.beacon, sm.GetNetworkVersion) - return r.GetChainRandomness(ctx, randEpoch) } diff --git a/conformance/rand_fixed.go b/conformance/rand_fixed.go index f35f05cd4ff..6e32c7555bf 100644 --- a/conformance/rand_fixed.go +++ b/conformance/rand_fixed.go @@ -6,6 +6,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/lotus/chain/rand" + "github.com/filecoin-project/lotus/chain/types" ) type fixedRand struct{} @@ -22,6 +23,10 @@ func (r *fixedRand) GetChainRandomness(_ context.Context, _ abi.ChainEpoch) ([32 return *(*[32]byte)([]byte("i_am_random_____i_am_random_____")), nil } +func (r *fixedRand) GetBeaconEntry(_ context.Context, _ abi.ChainEpoch) (*types.BeaconEntry, error) { + return &types.BeaconEntry{Round: 10, Data: []byte("i_am_random_____i_am_random_____")}, nil +} + func (r *fixedRand) GetBeaconRandomness(_ context.Context, _ abi.ChainEpoch) ([32]byte, error) { return *(*[32]byte)([]byte("i_am_random_____i_am_random_____")), nil // 32 bytes. } diff --git a/conformance/rand_record.go b/conformance/rand_record.go index 4dc30b28ebf..7364970a19e 100644 --- a/conformance/rand_record.go +++ b/conformance/rand_record.go @@ -74,7 +74,7 @@ func (r *RecordingRand) GetBeaconRandomness(ctx context.Context, round abi.Chain return [32]byte{}, err } - r.reporter.Logf("fetched and recorded beacon randomness for: epoch=%d, result=%x", round, ret) + r.reporter.Logf("fetched and recorded beacon randomness for: epoch=%d, result=%x", round, ret) match := schema.RandomnessMatch{ On: schema.RandomnessRule{ @@ -90,6 +90,29 @@ func (r *RecordingRand) GetBeaconRandomness(ctx context.Context, round abi.Chain return *(*[32]byte)(ret), err } +func (r *RecordingRand) GetBeaconEntry(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) { + r.once.Do(r.loadHead) + ret, err := r.api.StateGetBeaconEntry(ctx, round) + if err != nil { + return nil, err + } + + r.reporter.Logf("fetched and recorded beacon randomness for: epoch=%d, result=%x", round, ret) + + match := schema.RandomnessMatch{ + On: schema.RandomnessRule{ + Kind: schema.RandomnessBeacon, + Epoch: int64(round), + }, + Return: ret.Data, + } + r.lk.Lock() + r.recorded = append(r.recorded, match) + r.lk.Unlock() + + return ret, err +} + func (r *RecordingRand) Recorded() schema.Randomness { r.lk.Lock() defer r.lk.Unlock() diff --git a/conformance/rand_replay.go b/conformance/rand_replay.go index 6d78d813b8a..21601d1d9f3 100644 --- a/conformance/rand_replay.go +++ b/conformance/rand_replay.go @@ -7,6 +7,7 @@ import ( "github.com/filecoin-project/test-vectors/schema" "github.com/filecoin-project/lotus/chain/rand" + "github.com/filecoin-project/lotus/chain/types" ) type ReplayingRand struct { @@ -61,7 +62,7 @@ func (r *ReplayingRand) GetBeaconRandomness(ctx context.Context, round abi.Chain } if ret, ok := r.match(rule); ok { - r.reporter.Logf("returning saved beacon randomness: epoch=%d, result=%x", round, ret) + r.reporter.Logf("returning saved beacon randomness: epoch=%d, result=%x", round, ret) return ret, nil } @@ -69,3 +70,19 @@ func (r *ReplayingRand) GetBeaconRandomness(ctx context.Context, round abi.Chain return r.fallback.GetBeaconRandomness(ctx, round) } + +func (r *ReplayingRand) GetBeaconEntry(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) { + rule := schema.RandomnessRule{ + Kind: schema.RandomnessBeacon, + Epoch: int64(round), + } + + if ret, ok := r.match(rule); ok { + r.reporter.Logf("returning saved beacon randomness: epoch=%d, result=%x", round, ret) + return &types.BeaconEntry{Round: 10, Data: ret[:]}, nil + } + + r.reporter.Logf("returning fallback beacon randomness: epoch=%d, ", round) + + return r.fallback.GetBeaconEntry(ctx, round) +} diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index da322b4882d..b943b6611f3 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -6361,9 +6361,10 @@ Inputs: Response: `{}` ### StateGetBeaconEntry -StateGetBeaconEntry returns the beacon entry for the given filecoin epoch. If -the entry has not yet been produced, the call will block until the entry -becomes available +StateGetBeaconEntry returns the beacon entry for the given filecoin epoch +by using the recorded entries on the chain. If the entry for the requested +epoch has not yet been produced, the call will block until the entry +becomes available. Perms: read diff --git a/node/impl/full/state.go b/node/impl/full/state.go index 0c70d66a915..fbd3af58d57 100644 --- a/node/impl/full/state.go +++ b/node/impl/full/state.go @@ -1904,6 +1904,16 @@ func (a *StateAPI) StateGetRandomnessDigestFromBeacon(ctx context.Context, randE } func (a *StateAPI) StateGetBeaconEntry(ctx context.Context, epoch abi.ChainEpoch) (*types.BeaconEntry, error) { + if epoch <= a.Chain.GetHeaviestTipSet().Height() { + if epoch < 0 { + epoch = 0 + } + // get the beacon entry off the chain + return a.StateManager.GetBeaconEntry(ctx, epoch, types.EmptyTSK) + } + + // else we're asking for the future, get it from drand and block until it arrives + b := a.Beacon.BeaconForEpoch(epoch) rr := b.MaxBeaconRoundForEpoch(a.StateManager.GetNetworkVersion(ctx, epoch), epoch) e := b.Entry(ctx, rr) diff --git a/node/impl/full/state_test.go b/node/impl/full/state_test.go new file mode 100644 index 00000000000..372613ad332 --- /dev/null +++ b/node/impl/full/state_test.go @@ -0,0 +1,282 @@ +package full_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" + + "github.com/filecoin-project/lotus/chain/actors/policy" + "github.com/filecoin-project/lotus/chain/beacon" + "github.com/filecoin-project/lotus/chain/consensus/filcns" + "github.com/filecoin-project/lotus/chain/gen" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/node/impl/full" +) + +func init() { + policy.SetSupportedProofTypes(abi.RegisteredSealProof_StackedDrg2KiBV1) + policy.SetConsensusMinerMinPower(abi.NewStoragePower(2048)) + policy.SetMinVerifiedDealSize(abi.NewStoragePower(256)) +} + +// similar to chain/rand/rand_test.go +func TestStateGetBeaconEntry(t *testing.T) { + // Ref: https://github.com/filecoin-project/lotus/issues/12414#issuecomment-2320034935 + type expectedBeaconStrategy int + const ( + expectedBeaconStrategy_beforeNulls expectedBeaconStrategy = iota + expectedBeaconStrategy_afterNulls + expectedBeaconStrategy_exact + ) + + testCases := []struct { + name string + nv network.Version + strategy expectedBeaconStrategy // how to determine which round to expect + wait bool // whether the test should wait for a future round + negativeEpoch bool + }{ + { + // In v12 and before, if the tipset corresponding to round X is null, we fetch the latest beacon entry BEFORE X that's in a non-null ts + name: "pre-nv12@1 nulls", + nv: network.Version1, + strategy: expectedBeaconStrategy_beforeNulls, + }, + { + name: "pre-nv12@9 nulls", + nv: network.Version9, + strategy: expectedBeaconStrategy_beforeNulls, + }, + { + name: "pre-nv12@10 nulls", + nv: network.Version10, + strategy: expectedBeaconStrategy_beforeNulls, + }, + { + name: "pre-nv12@12 nulls", + nv: network.Version12, + strategy: expectedBeaconStrategy_beforeNulls, + }, + { + name: "pre-nv12 wait for future round", + nv: network.Version12, + strategy: expectedBeaconStrategy_exact, + wait: true, + }, + { + name: "pre-nv12 requesting negative epoch", + nv: network.Version12, + negativeEpoch: true, + }, + { + // At v13, if the tipset corresponding to round X is null, we fetch the latest beacon entry in the first non-null ts after X + name: "nv13 nulls", + nv: network.Version13, + strategy: expectedBeaconStrategy_afterNulls, + }, + { + name: "nv13 requesting negative epoch", + nv: network.Version13, + negativeEpoch: true, + }, + { + name: "nv13 wait for future round", + nv: network.Version13, + strategy: expectedBeaconStrategy_exact, + wait: true, + }, + { + // After v14, if the tipset corresponding to round X is null, we still fetch the randomness for X (from the next non-null tipset) but can get the exact round + name: "nv14+ nulls", + nv: network.Version14, + strategy: expectedBeaconStrategy_exact, + }, + { + name: "nv14+ wait for future round", + nv: network.Version14, + strategy: expectedBeaconStrategy_exact, + wait: true, + }, + { + name: "nv14 requesting negative epoch", + nv: network.Version14, + negativeEpoch: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + req := require.New(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Setup the necessary (and usable upgrades) to test what we need + upgrades := stmgr.UpgradeSchedule{} + for _, upg := range []stmgr.Upgrade{ + { + Network: network.Version9, + Height: 1, + Migration: filcns.UpgradeActorsV2, + }, { + Network: network.Version10, + Height: 2, + Migration: filcns.UpgradeActorsV3, + }, { + Network: network.Version12, + Height: 3, + Migration: filcns.UpgradeActorsV4, + }, { + Network: network.Version13, + Height: 4, + Migration: filcns.UpgradeActorsV5, + }, { + Network: network.Version14, + Height: 5, + Migration: filcns.UpgradeActorsV6, + }, + } { + if upg.Network > tc.nv { + break + } + upgrades = append(upgrades, upg) + } + + // New chain generator + cg, err := gen.NewGeneratorWithUpgradeSchedule(upgrades) + req.NoError(err) + + // Mine enough blocks to get through any upgrades + for i := 0; i < 10; i++ { + _, err := cg.NextTipSet() + req.NoError(err) + } + + heightBeforeNulls := cg.CurTipset.TipSet().Height() + + // Mine a new block but behave as if there were 5 null blocks before it + ts, err := cg.NextTipSetWithNulls(5) + req.NoError(err) + + // Offset of drand epoch to filecoin epoch for easier calculation later + drandOffset := cg.CurTipset.Blocks[0].Header.BeaconEntries[len(cg.CurTipset.Blocks[0].Header.BeaconEntries)-1].Round - uint64(cg.CurTipset.TipSet().Height()) + // Epoch at which we want to get the beacon entry + randEpoch := ts.TipSet.TipSet().Height() - 2 + + mockBeacon := cg.BeaconSchedule()[0].Beacon.(*beacon.MockBeacon) + if tc.wait { + randEpoch = ts.TipSet.TipSet().Height() + 1 // in the future + // Set the max index to the height of the tipset + the offset to make the calls block, waiting for a future round + mockBeacon.SetMaxIndex(int(ts.TipSet.TipSet().Height())+int(drandOffset), false) + } + + state := &full.StateAPI{ + Chain: cg.ChainStore(), + StateManager: cg.StateManager(), + Beacon: cg.BeaconSchedule(), + } + + // We will be performing two beacon look-ups in separate goroutines, where tc.wait is true we + // expect them both to block until we tell the mock beacon to return the beacon entry. + // Otherwise they should both return immediately. + + var gotBeacon *beacon.Response + var expectedBeacon *beacon.Response + gotDoneCh := make(chan struct{}) + expectedDoneCh := make(chan struct{}) + + // Get the beacon entry from the state API + go func() { + reqEpoch := randEpoch + if tc.negativeEpoch { + reqEpoch = abi.ChainEpoch(-1) + } + be, err := state.StateGetBeaconEntry(ctx, reqEpoch) + if err != nil { + gotBeacon = &beacon.Response{Err: err} + } else { + gotBeacon = &beacon.Response{Entry: *be} + } + close(gotDoneCh) + }() + + // Get the beacon entry directly from the beacon. + + // First, determine which round to expect based on the strategy for the given network version + var beaconRound uint64 + switch tc.strategy { + case expectedBeaconStrategy_beforeNulls: + beaconRound = uint64(heightBeforeNulls) + case expectedBeaconStrategy_afterNulls: + beaconRound = uint64(ts.TipSet.TipSet().Height()) + case expectedBeaconStrategy_exact: + beaconRound = uint64(randEpoch) + } + + if tc.negativeEpoch { + // A negative epoch should get the genesis beacon, which is hardwired to round 0, all zeros + // in our test data + expectedBeacon = &beacon.Response{Entry: types.BeaconEntry{Data: make([]byte, 32), Round: 0}} + close(expectedDoneCh) + } else { + bch := cg.BeaconSchedule().BeaconForEpoch(randEpoch).Entry(ctx, beaconRound+drandOffset) + go func() { + select { + case resp := <-bch: + expectedBeacon = &resp + case <-ctx.Done(): + req.Fail("timed out") + } + close(expectedDoneCh) + }() + } + + if tc.wait { + // Wait for the beacon entry to be requested by both the StateGetBeaconEntry call and the + // BeaconForEpoch.Entry call to be blocking + req.Eventually(func() bool { + return mockBeacon.WaitingOnEntryCount() == 2 + }, 5*time.Second, 10*time.Millisecond) + + // just to be sure, make sure the calls are still blocking + select { + case <-gotDoneCh: + req.Fail("should not have received beacon entry yet") + default: + } + select { + case <-expectedDoneCh: + req.Fail("should not have received beacon entry yet") + default: + } + + // Increment the max index to allow the mock beacon to return the beacon entry to both calls + mockBeacon.SetMaxIndex(int(ts.TipSet.TipSet().Height())+int(drandOffset)+1, true) + } + + select { + case <-gotDoneCh: + case <-ctx.Done(): + req.Fail("timed out") + } + req.NoError(gotBeacon.Err) + select { + case <-expectedDoneCh: + case <-ctx.Done(): + req.Fail("timed out") + } + req.NoError(expectedBeacon.Err) + + req.Equal(0, mockBeacon.WaitingOnEntryCount()) // both should be unblocked + + // Compare the expected beacon entry with the one we got + require.Equal(t, gotBeacon.Entry, expectedBeacon.Entry) + }) + } +}