Skip to content

Commit e16e08a

Browse files
authored
fix(drand): StateGetBeaconEntry uses chain beacons for historical epochs (#12428)
* fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs Fixes: #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 #12414 (comment) for specifics. StateGetBeaconEntry still blocks for future epochs and uses live drand beacon clients to wait for and fetch rounds as they are available. * fixup! fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs * fixup! fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs
1 parent f33d4a2 commit e16e08a

File tree

14 files changed

+479
-82
lines changed

14 files changed

+479
-82
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Add `EthGetBlockReceipts` RPC method to retrieve transaction receipts for a spec
1717
## Improvements
1818

1919
- 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))
20+
- 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)).
2021

2122
## Bug Fixes
2223

api/api_full.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,10 @@ type FullNode interface {
592592
// StateGetRandomnessDigestFromBeacon is used to sample the beacon for randomness.
593593
StateGetRandomnessDigestFromBeacon(ctx context.Context, randEpoch abi.ChainEpoch, tsk types.TipSetKey) (abi.Randomness, error) //perm:read
594594

595-
// StateGetBeaconEntry returns the beacon entry for the given filecoin epoch. If
596-
// the entry has not yet been produced, the call will block until the entry
597-
// becomes available
595+
// StateGetBeaconEntry returns the beacon entry for the given filecoin epoch
596+
// by using the recorded entries on the chain. If the entry for the requested
597+
// epoch has not yet been produced, the call will block until the entry
598+
// becomes available.
598599
StateGetBeaconEntry(ctx context.Context, epoch abi.ChainEpoch) (*types.BeaconEntry, error) //perm:read
599600

600601
// StateGetNetworkParams return current network params

build/openrpc/full.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19266,7 +19266,7 @@
1926619266
{
1926719267
"name": "Filecoin.StateGetBeaconEntry",
1926819268
"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```",
19269-
"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",
19269+
"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",
1927019270
"paramStructure": "by-position",
1927119271
"params": [
1927219272
{

chain/beacon/mock.go

+61-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"encoding/binary"
7+
"sync"
78
"time"
89

910
"golang.org/x/crypto/blake2b"
@@ -15,26 +16,54 @@ import (
1516
"github.com/filecoin-project/lotus/chain/types"
1617
)
1718

18-
// mockBeacon assumes that filecoin rounds are 1:1 mapped with the beacon rounds
19-
type mockBeacon struct {
20-
interval time.Duration
19+
// MockBeacon assumes that filecoin rounds are 1:1 mapped with the beacon rounds
20+
type MockBeacon struct {
21+
interval time.Duration
22+
maxIndex int
23+
waitingEntry int
24+
lk sync.Mutex
25+
cond *sync.Cond
2126
}
2227

23-
func (mb *mockBeacon) IsChained() bool {
28+
func (mb *MockBeacon) IsChained() bool {
2429
return true
2530
}
2631

2732
func NewMockBeacon(interval time.Duration) RandomBeacon {
28-
mb := &mockBeacon{interval: interval}
29-
33+
mb := &MockBeacon{interval: interval, maxIndex: -1}
34+
mb.cond = sync.NewCond(&mb.lk)
3035
return mb
3136
}
3237

33-
func (mb *mockBeacon) RoundTime() time.Duration {
38+
// SetMaxIndex sets the maximum index that the beacon will return, and optionally blocks until all
39+
// waiting requests are satisfied. If maxIndex is -1, the beacon will return entries indefinitely.
40+
func (mb *MockBeacon) SetMaxIndex(maxIndex int, blockTillNoneWaiting bool) {
41+
mb.lk.Lock()
42+
defer mb.lk.Unlock()
43+
mb.maxIndex = maxIndex
44+
mb.cond.Broadcast()
45+
if !blockTillNoneWaiting {
46+
return
47+
}
48+
49+
for mb.waitingEntry > 0 {
50+
mb.cond.Wait()
51+
}
52+
}
53+
54+
// WaitingOnEntryCount returns the number of requests that are currently waiting for an entry. Where
55+
// maxIndex has not been set, this will always return 0 as beacon entries are generated on demand.
56+
func (mb *MockBeacon) WaitingOnEntryCount() int {
57+
mb.lk.Lock()
58+
defer mb.lk.Unlock()
59+
return mb.waitingEntry
60+
}
61+
62+
func (mb *MockBeacon) RoundTime() time.Duration {
3463
return mb.interval
3564
}
3665

37-
func (mb *mockBeacon) entryForIndex(index uint64) types.BeaconEntry {
66+
func (mb *MockBeacon) entryForIndex(index uint64) types.BeaconEntry {
3867
buf := make([]byte, 8)
3968
binary.BigEndian.PutUint64(buf, index)
4069
rval := blake2b.Sum256(buf)
@@ -44,14 +73,32 @@ func (mb *mockBeacon) entryForIndex(index uint64) types.BeaconEntry {
4473
}
4574
}
4675

47-
func (mb *mockBeacon) Entry(ctx context.Context, index uint64) <-chan Response {
48-
e := mb.entryForIndex(index)
76+
func (mb *MockBeacon) Entry(ctx context.Context, index uint64) <-chan Response {
4977
out := make(chan Response, 1)
50-
out <- Response{Entry: e}
78+
79+
mb.lk.Lock()
80+
defer mb.lk.Unlock()
81+
82+
if mb.maxIndex >= 0 && index > uint64(mb.maxIndex) {
83+
mb.waitingEntry++
84+
go func() {
85+
mb.lk.Lock()
86+
defer mb.lk.Unlock()
87+
for index > uint64(mb.maxIndex) {
88+
mb.cond.Wait()
89+
}
90+
out <- Response{Entry: mb.entryForIndex(index)}
91+
mb.waitingEntry--
92+
mb.cond.Broadcast()
93+
}()
94+
} else {
95+
out <- Response{Entry: mb.entryForIndex(index)}
96+
}
97+
5198
return out
5299
}
53100

54-
func (mb *mockBeacon) VerifyEntry(from types.BeaconEntry, _prevEntrySig []byte) error {
101+
func (mb *MockBeacon) VerifyEntry(from types.BeaconEntry, _prevEntrySig []byte) error {
55102
// TODO: cache this, especially for bls
56103
oe := mb.entryForIndex(from.Round)
57104
if !bytes.Equal(from.Data, oe.Data) {
@@ -60,9 +107,9 @@ func (mb *mockBeacon) VerifyEntry(from types.BeaconEntry, _prevEntrySig []byte)
60107
return nil
61108
}
62109

63-
func (mb *mockBeacon) MaxBeaconRoundForEpoch(nv network.Version, epoch abi.ChainEpoch) uint64 {
110+
func (mb *MockBeacon) MaxBeaconRoundForEpoch(nv network.Version, epoch abi.ChainEpoch) uint64 {
64111
// offset for better testing
65112
return uint64(epoch + 100)
66113
}
67114

68-
var _ RandomBeacon = (*mockBeacon)(nil)
115+
var _ RandomBeacon = (*MockBeacon)(nil)

chain/gen/genesis/miners.go

+5
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,11 @@ func (fr *fakeRand) GetChainRandomness(ctx context.Context, randEpoch abi.ChainE
647647
return *(*[32]byte)(out), nil
648648
}
649649

650+
func (fr *fakeRand) GetBeaconEntry(ctx context.Context, randEpoch abi.ChainEpoch) (*types.BeaconEntry, error) {
651+
r, _ := fr.GetChainRandomness(ctx, randEpoch)
652+
return &types.BeaconEntry{Round: 10, Data: r[:]}, nil
653+
}
654+
650655
func (fr *fakeRand) GetBeaconRandomness(ctx context.Context, randEpoch abi.ChainEpoch) ([32]byte, error) {
651656
out := make([]byte, 32)
652657
_, _ = rand.New(rand.NewSource(int64(randEpoch))).Read(out) //nolint

chain/rand/rand.go

+51-57
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type stateRand struct {
111111

112112
type Rand interface {
113113
GetChainRandomness(ctx context.Context, round abi.ChainEpoch) ([32]byte, error)
114+
GetBeaconEntry(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error)
114115
GetBeaconRandomness(ctx context.Context, round abi.ChainEpoch) ([32]byte, error)
115116
}
116117

@@ -124,48 +125,58 @@ func NewStateRand(cs *store.ChainStore, blks []cid.Cid, b beacon.Schedule, netwo
124125
}
125126

126127
// network v0-12
127-
func (sr *stateRand) getBeaconRandomnessV1(ctx context.Context, round abi.ChainEpoch) ([32]byte, error) {
128+
func (sr *stateRand) getBeaconEntryV1(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) {
128129
randTs, err := sr.GetBeaconRandomnessTipset(ctx, round, true)
129130
if err != nil {
130-
return [32]byte{}, err
131-
}
132-
133-
be, err := sr.cs.GetLatestBeaconEntry(ctx, randTs)
134-
if err != nil {
135-
return [32]byte{}, err
131+
return nil, err
136132
}
137-
138-
return blake2b.Sum256(be.Data), nil
133+
return sr.cs.GetLatestBeaconEntry(ctx, randTs)
139134
}
140135

141136
// network v13
142-
func (sr *stateRand) getBeaconRandomnessV2(ctx context.Context, round abi.ChainEpoch) ([32]byte, error) {
137+
func (sr *stateRand) getBeaconEntryV2(ctx context.Context, round abi.ChainEpoch) (*types.BeaconEntry, error) {
143138
randTs, err := sr.GetBeaconRandomnessTipset(ctx, round, false)
144139
if err != nil {
145-
return [32]byte{}, err
146-
}
147-
148-
be, err := sr.cs.GetLatestBeaconEntry(ctx, randTs)
149-
if err != nil {
150-
return [32]byte{}, err
140+
return nil, err
151141
}
152-
153-
return blake2b.Sum256(be.Data), nil
142+
return sr.cs.GetLatestBeaconEntry(ctx, randTs)
154143
}
155144

156145
// network v14 and on
157-
func (sr *stateRand) getBeaconRandomnessV3(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) {
146+
func (sr *stateRand) getBeaconEntryV3(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) {
158147
if filecoinEpoch < 0 {
159-
return sr.getBeaconRandomnessV2(ctx, filecoinEpoch)
148+
return sr.getBeaconEntryV2(ctx, filecoinEpoch)
160149
}
161150

162-
be, err := sr.extractBeaconEntryForEpoch(ctx, filecoinEpoch)
151+
randTs, err := sr.GetBeaconRandomnessTipset(ctx, filecoinEpoch, false)
163152
if err != nil {
164-
log.Errorf("failed to get beacon entry as expected: %s", err)
165-
return [32]byte{}, err
153+
return nil, err
166154
}
167155

168-
return blake2b.Sum256(be.Data), nil
156+
nv := sr.networkVersionGetter(ctx, filecoinEpoch)
157+
158+
round := sr.beacon.BeaconForEpoch(filecoinEpoch).MaxBeaconRoundForEpoch(nv, filecoinEpoch)
159+
160+
// Search back for the beacon entry, in normal operation it should be in randTs but for devnets
161+
// where the blocktime is faster than the beacon period we may need to search back a bit to find
162+
// the beacon entry for the requested round.
163+
for i := 0; i < 20; i++ {
164+
cbe := randTs.Blocks()[0].BeaconEntries
165+
for _, v := range cbe {
166+
if v.Round == round {
167+
return &v, nil
168+
}
169+
}
170+
171+
next, err := sr.cs.LoadTipSet(ctx, randTs.Parents())
172+
if err != nil {
173+
return nil, xerrors.Errorf("failed to load parents when searching back for beacon entry: %w", err)
174+
}
175+
176+
randTs = next
177+
}
178+
179+
return nil, xerrors.Errorf("didn't find beacon for round %d (epoch %d)", round, filecoinEpoch)
169180
}
170181

171182
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
178189
return sr.getChainRandomness(ctx, filecoinEpoch, true)
179190
}
180191

181-
func (sr *stateRand) GetBeaconRandomness(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) {
192+
func (sr *stateRand) GetBeaconEntry(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) {
182193
nv := sr.networkVersionGetter(ctx, filecoinEpoch)
183194

184195
if nv >= network.Version14 {
185-
return sr.getBeaconRandomnessV3(ctx, filecoinEpoch)
196+
be, err := sr.getBeaconEntryV3(ctx, filecoinEpoch)
197+
if err != nil {
198+
log.Errorf("failed to get beacon entry as expected: %s", err)
199+
}
200+
return be, err
186201
} else if nv == network.Version13 {
187-
return sr.getBeaconRandomnessV2(ctx, filecoinEpoch)
202+
return sr.getBeaconEntryV2(ctx, filecoinEpoch)
203+
}
204+
return sr.getBeaconEntryV1(ctx, filecoinEpoch)
205+
}
206+
207+
func (sr *stateRand) GetBeaconRandomness(ctx context.Context, filecoinEpoch abi.ChainEpoch) ([32]byte, error) {
208+
be, err := sr.GetBeaconEntry(ctx, filecoinEpoch)
209+
if err != nil {
210+
return [32]byte{}, err
188211
}
189-
return sr.getBeaconRandomnessV1(ctx, filecoinEpoch)
212+
return blake2b.Sum256(be.Data), nil
190213
}
191214

192215
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
218241

219242
return ret, nil
220243
}
221-
222-
func (sr *stateRand) extractBeaconEntryForEpoch(ctx context.Context, filecoinEpoch abi.ChainEpoch) (*types.BeaconEntry, error) {
223-
randTs, err := sr.GetBeaconRandomnessTipset(ctx, filecoinEpoch, false)
224-
if err != nil {
225-
return nil, err
226-
}
227-
228-
nv := sr.networkVersionGetter(ctx, filecoinEpoch)
229-
230-
round := sr.beacon.BeaconForEpoch(filecoinEpoch).MaxBeaconRoundForEpoch(nv, filecoinEpoch)
231-
232-
for i := 0; i < 20; i++ {
233-
cbe := randTs.Blocks()[0].BeaconEntries
234-
for _, v := range cbe {
235-
if v.Round == round {
236-
return &v, nil
237-
}
238-
}
239-
240-
next, err := sr.cs.LoadTipSet(ctx, randTs.Parents())
241-
if err != nil {
242-
return nil, xerrors.Errorf("failed to load parents when searching back for beacon entry: %w", err)
243-
}
244-
245-
randTs = next
246-
}
247-
248-
return nil, xerrors.Errorf("didn't find beacon for round %d (epoch %d)", round, filecoinEpoch)
249-
}

chain/stmgr/stmgr.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -572,9 +572,17 @@ func (sm *StateManager) GetRandomnessDigestFromBeacon(ctx context.Context, randE
572572
}
573573

574574
r := rand.NewStateRand(sm.ChainStore(), pts.Cids(), sm.beacon, sm.GetNetworkVersion)
575-
576575
return r.GetBeaconRandomness(ctx, randEpoch)
576+
}
577577

578+
func (sm *StateManager) GetBeaconEntry(ctx context.Context, randEpoch abi.ChainEpoch, tsk types.TipSetKey) (*types.BeaconEntry, error) {
579+
pts, err := sm.ChainStore().GetTipSetFromKey(ctx, tsk)
580+
if err != nil {
581+
return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err)
582+
}
583+
584+
r := rand.NewStateRand(sm.ChainStore(), pts.Cids(), sm.beacon, sm.GetNetworkVersion)
585+
return r.GetBeaconEntry(ctx, randEpoch)
578586
}
579587

580588
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
584592
}
585593

586594
r := rand.NewStateRand(sm.ChainStore(), pts.Cids(), sm.beacon, sm.GetNetworkVersion)
587-
588595
return r.GetChainRandomness(ctx, randEpoch)
589596
}

chain/store/store.go

+4
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,10 @@ func (cs *ChainStore) GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey)
13441344

13451345
func (cs *ChainStore) GetLatestBeaconEntry(ctx context.Context, ts *types.TipSet) (*types.BeaconEntry, error) {
13461346
cur := ts
1347+
1348+
// Search for a beacon entry, in normal operation one should be in the requested tipset, but for
1349+
// devnets where the blocktime is faster than the beacon period we may need to search back a bit
1350+
// to find a tipset with a beacon entry.
13471351
for i := 0; i < 20; i++ {
13481352
cbe := cur.Blocks()[0].BeaconEntries
13491353
if len(cbe) > 0 {

conformance/rand_fixed.go

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/filecoin-project/go-state-types/abi"
77

88
"github.com/filecoin-project/lotus/chain/rand"
9+
"github.com/filecoin-project/lotus/chain/types"
910
)
1011

1112
type fixedRand struct{}
@@ -22,6 +23,10 @@ func (r *fixedRand) GetChainRandomness(_ context.Context, _ abi.ChainEpoch) ([32
2223
return *(*[32]byte)([]byte("i_am_random_____i_am_random_____")), nil
2324
}
2425

26+
func (r *fixedRand) GetBeaconEntry(_ context.Context, _ abi.ChainEpoch) (*types.BeaconEntry, error) {
27+
return &types.BeaconEntry{Round: 10, Data: []byte("i_am_random_____i_am_random_____")}, nil
28+
}
29+
2530
func (r *fixedRand) GetBeaconRandomness(_ context.Context, _ abi.ChainEpoch) ([32]byte, error) {
2631
return *(*[32]byte)([]byte("i_am_random_____i_am_random_____")), nil // 32 bytes.
2732
}

0 commit comments

Comments
 (0)