Files
lotus/node/impl/full/state_test.go
Rod Vagg e16e08a7a6 fix(drand): StateGetBeaconEntry uses chain beacons for historical epochs (#12428)
* 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
2024-09-27 12:55:14 +02:00

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