diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3583336b1..cd304bac29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Add `EthGetBlockReceipts` RPC method to retrieve transaction receipts for a spec ## 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)). ## Bug Fixes diff --git a/api/api_full.go b/api/api_full.go index 7ff5dd4e8e8..14021f50471 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -592,9 +592,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 7023b3832ec..b0a861ecc33 100644 --- a/build/openrpc/full.json +++ b/build/openrpc/full.json @@ -19266,7 +19266,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..505b779dc5b 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,58 @@ 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 - } - - 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 v14 and on -func (sr *stateRand) getBeaconRandomnessV3(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) { +func (sr *stateRand) getBeaconEntryV3(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) { if filecoinEpoch < 0 { - return sr.getBeaconRandomnessV2(ctx, filecoinEpoch) + return sr.getBeaconEntryV2(ctx, filecoinEpoch) } - be, err := sr.extractBeaconEntryForEpoch(ctx, filecoinEpoch) + randTs, err := sr.GetBeaconRandomnessTipset(ctx, filecoinEpoch, false) if err != nil { - log.Errorf("failed to get beacon entry as expected: %s", err) - return [32]byte{}, err + return nil, err } - return blake2b.Sum256(be.Data), nil + nv := sr.networkVersionGetter(ctx, filecoinEpoch) + + round := sr.beacon.BeaconForEpoch(filecoinEpoch).MaxBeaconRoundForEpoch(nv, filecoinEpoch) + + // Search back for the beacon entry, in normal operation it should be in randTs but for devnets + // where the blocktime is faster than the beacon period we may need to search back a bit to find + // the beacon entry for the requested round. + 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) } func (sr *stateRand) GetChainRandomness(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) { @@ -178,15 +189,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) + 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 { - return sr.getBeaconRandomnessV2(ctx, filecoinEpoch) + 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 +241,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/chain/store/store.go b/chain/store/store.go index 18823b574bf..1d2329ecfab 100644 --- a/chain/store/store.go +++ b/chain/store/store.go @@ -1344,6 +1344,10 @@ func (cs *ChainStore) GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey) func (cs *ChainStore) GetLatestBeaconEntry(ctx context.Context, ts *types.TipSet) (*types.BeaconEntry, error) { cur := ts + + // Search for a beacon entry, in normal operation one should be in the requested tipset, but for + // devnets where the blocktime is faster than the beacon period we may need to search back a bit + // to find a tipset with a beacon entry. for i := 0; i < 20; i++ { cbe := cur.Blocks()[0].BeaconEntries if len(cbe) > 0 { 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 d7d44fef571..1e4bf6b1821 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -6522,9 +6522,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 5660cbe3dd4..977e1655cf9 100644 --- a/node/impl/full/state.go +++ b/node/impl/full/state.go @@ -1956,6 +1956,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) + }) + } +}