197 lines
5.6 KiB
Go
197 lines
5.6 KiB
Go
package simulation
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
"github.com/filecoin-project/lotus/build"
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin/account"
|
|
"github.com/filecoin-project/lotus/chain/state"
|
|
"github.com/filecoin-project/lotus/chain/stmgr"
|
|
"github.com/filecoin-project/lotus/chain/store"
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
"github.com/filecoin-project/lotus/chain/vm"
|
|
)
|
|
|
|
const (
|
|
expectedBlocks = 5
|
|
// TODO: This will produce invalid blocks but it will accurately model the amount of gas
|
|
// we're willing to use per-tipset.
|
|
// A more correct approach would be to produce 5 blocks. We can do that later.
|
|
targetGas = build.BlockGasTarget * expectedBlocks
|
|
)
|
|
|
|
var baseFee = abi.NewTokenAmount(0)
|
|
|
|
// Step steps the simulation forward one step. This may move forward by more than one epoch.
|
|
func (sim *Simulation) Step(ctx context.Context) (*types.TipSet, error) {
|
|
state, err := sim.simState(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ts, err := state.step(ctx)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to step simulation: %w", err)
|
|
}
|
|
return ts, nil
|
|
}
|
|
|
|
func (ss *simulationState) step(ctx context.Context) (*types.TipSet, error) {
|
|
log.Infow("step", "epoch", ss.head.Height()+1)
|
|
messages, err := ss.popNextMessages(ctx)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to select messages for block: %w", err)
|
|
}
|
|
head, err := ss.makeTipSet(ctx, messages)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to make tipset: %w", err)
|
|
}
|
|
if err := ss.SetHead(head); err != nil {
|
|
return nil, xerrors.Errorf("failed to update head: %w", err)
|
|
}
|
|
return head, nil
|
|
}
|
|
|
|
type packFunc func(*types.Message) (full bool, err error)
|
|
type messageGenerator func(ctx context.Context, cb packFunc) (full bool, err error)
|
|
|
|
func (ss *simulationState) popNextMessages(ctx context.Context) ([]*types.Message, error) {
|
|
parentTs := ss.head
|
|
parentState, _, err := ss.sm.TipSetState(ctx, parentTs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nextHeight := parentTs.Height() + 1
|
|
prevVer := ss.sm.GetNtwkVersion(ctx, nextHeight-1)
|
|
nextVer := ss.sm.GetNtwkVersion(ctx, nextHeight)
|
|
if nextVer != prevVer {
|
|
// So... we _could_ actually run the migration, but that's a pain. It's easier to
|
|
// just have an empty block then let the state manager run the migration as normal.
|
|
log.Warnw("packing no messages for version upgrade block",
|
|
"old", prevVer,
|
|
"new", nextVer,
|
|
"epoch", nextHeight,
|
|
)
|
|
return nil, nil
|
|
}
|
|
|
|
// Then we need to execute messages till we run out of gas. Those messages will become the
|
|
// block's messages.
|
|
r := store.NewChainRand(ss.sm.ChainStore(), parentTs.Cids())
|
|
// TODO: Factor this out maybe?
|
|
vmopt := &vm.VMOpts{
|
|
StateBase: parentState,
|
|
Epoch: nextHeight,
|
|
Rand: r,
|
|
Bstore: ss.sm.ChainStore().StateBlockstore(),
|
|
Syscalls: ss.sm.ChainStore().VMSys(),
|
|
CircSupplyCalc: ss.sm.GetVMCirculatingSupply,
|
|
NtwkVersion: ss.sm.GetNtwkVersion,
|
|
BaseFee: abi.NewTokenAmount(0), // FREE!
|
|
LookbackState: stmgr.LookbackStateGetterForTipset(ss.sm, parentTs),
|
|
}
|
|
vmi, err := vm.NewVM(ctx, vmopt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO: This is the wrong store and may not include important state for what we're doing
|
|
// here....
|
|
// Maybe we just track nonces separately? Yeah, probably better that way.
|
|
vmStore := vmi.ActorStore(ctx)
|
|
var gasTotal int64
|
|
var messages []*types.Message
|
|
tryPushMsg := func(msg *types.Message) (bool, error) {
|
|
if gasTotal >= targetGas {
|
|
return true, nil
|
|
}
|
|
|
|
// Copy the message before we start mutating it.
|
|
msgCpy := *msg
|
|
msg = &msgCpy
|
|
st := vmi.StateTree().(*state.StateTree)
|
|
|
|
actor, err := st.GetActor(msg.From)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
msg.Nonce = actor.Nonce
|
|
if msg.From.Protocol() == address.ID {
|
|
state, err := account.Load(vmStore, actor)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
msg.From, err = state.PubkeyAddress()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
// TODO: Our gas estimation is broken for payment channels due to horrible hacks in
|
|
// gasEstimateGasLimit.
|
|
if msg.Value == types.EmptyInt {
|
|
msg.Value = abi.NewTokenAmount(0)
|
|
}
|
|
msg.GasPremium = abi.NewTokenAmount(0)
|
|
msg.GasFeeCap = abi.NewTokenAmount(0)
|
|
msg.GasLimit = build.BlockGasLimit
|
|
|
|
// We manually snapshot so we can revert nonce changes, etc. on failure.
|
|
st.Snapshot(ctx)
|
|
defer st.ClearSnapshot()
|
|
|
|
ret, err := vmi.ApplyMessage(ctx, msg)
|
|
if err != nil {
|
|
_ = st.Revert()
|
|
return false, err
|
|
}
|
|
if ret.ActorErr != nil {
|
|
_ = st.Revert()
|
|
return false, ret.ActorErr
|
|
}
|
|
|
|
// Sometimes there are bugs. Let's catch them.
|
|
if ret.GasUsed == 0 {
|
|
_ = st.Revert()
|
|
return false, xerrors.Errorf("used no gas",
|
|
"msg", msg,
|
|
"ret", ret,
|
|
)
|
|
}
|
|
|
|
// TODO: consider applying overestimation? We're likely going to "over pack" here by
|
|
// ~25% because we're too accurate.
|
|
|
|
// Did we go over? Yes, revert.
|
|
newTotal := gasTotal + ret.GasUsed
|
|
if newTotal > targetGas {
|
|
_ = st.Revert()
|
|
return true, nil
|
|
}
|
|
gasTotal = newTotal
|
|
|
|
// Update the gas limit.
|
|
msg.GasLimit = ret.GasUsed
|
|
|
|
messages = append(messages, msg)
|
|
return false, nil
|
|
}
|
|
for _, mgen := range []messageGenerator{ss.packWindowPoSts, ss.packProveCommits, ss.packPreCommits} {
|
|
if full, err := mgen(ctx, tryPushMsg); err != nil {
|
|
name := runtime.FuncForPC(reflect.ValueOf(mgen).Pointer()).Name()
|
|
lastDot := strings.LastIndexByte(name, '.')
|
|
fName := name[lastDot+1 : len(name)-3]
|
|
return nil, xerrors.Errorf("when packing messages with %s: %w", fName, err)
|
|
} else if full {
|
|
break
|
|
}
|
|
}
|
|
|
|
return messages, nil
|
|
}
|