mirror of
https://github.com/filecoin-project/lotus.git
synced 2025-08-24 09:22:17 +08:00

* fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs Fixes: https://github.com/filecoin-project/lotus/issues/12414 Previously StateGetBeaconEntry would always try and use a drand beacon to get the appropriate round. But as drand has shut down old beacons and we've removed client details from Lotus, it has stopped working for historical beacons. This fix restores historical beacon entries by using the on-chain lookup, however it now follows the rules used by StateGetRandomnessFromBeacon and the get_beacon_randomness syscall which has some quirks with null rounds prior to nv14. See https://github.com/filecoin-project/lotus/issues/12414#issuecomment-2320034935 for specifics. StateGetBeaconEntry still blocks for future epochs and uses live drand beacon clients to wait for and fetch rounds as they are available. * fixup! fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs * fixup! fix(drand): `StateGetBeaconEntry` uses chain beacons for historical epochs
283 lines
8.4 KiB
Go
283 lines
8.4 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|