From d59800afb0601ee1c0d9248f799f2d7e5cc953f6 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 Ref: 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. --- api/api_full.go | 7 +- 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 ++- node/impl/full/state.go | 10 ++ node/impl/full/state_test.go | 280 +++++++++++++++++++++++++++++++++++ 10 files changed, 463 insertions(+), 81 deletions(-) create mode 100644 node/impl/full/state_test.go diff --git a/api/api_full.go b/api/api_full.go index 6b5e279a6bc..2a42cb2a7dc 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/chain/beacon/mock.go b/chain/beacon/mock.go index dfa036b9304..35a3412c142 100644 --- a/chain/beacon/mock.go +++ b/chain/beacon/mock.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/binary" + "sync" "time" "github.com/minio/blake2b-simd" @@ -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 f892d2aae1c..c4c1bad3a53 100644 --- a/chain/rand/rand.go +++ b/chain/rand/rand.go @@ -110,6 +110,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) } @@ -123,48 +124,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) { @@ -177,15 +181,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) { @@ -217,32 +233,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/node/impl/full/state.go b/node/impl/full/state.go index 80a744a0b2c..50e44755b00 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..63e4cc8d73d --- /dev/null +++ b/node/impl/full/state_test.go @@ -0,0 +1,280 @@ +package full_test + +import ( + "context" + "testing" + "time" + + "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" + "github.com/stretchr/testify/require" +) + +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) + }) + } +}