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) + }) + } +}