package simulation import ( "context" "errors" "reflect" "runtime" "strings" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" "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 ( // The number of expected blocks in a tipset. We use this to determine how much gas a tipset // has. 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 } // step steps the simulation state forward one step, producing and executing a new tipset. 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 } var ErrOutOfGas = errors.New("out of gas") // packFunc takes a message and attempts to pack it into a block. // // - If the block is full, returns the error ErrOutOfGas. // - If message execution fails, check if error is an ActorError to get the return code. type packFunc func(*types.Message) (*types.MessageReceipt, error) // popNextMessages generates/picks a set of messages to be included in the next block. // // - This function is destructive and should only be called once per epoch. // - This function does not store anything in the repo. // - This function handles all gas estimation. The returned messages should all fit in a single // block. func (ss *simulationState) popNextMessages(ctx context.Context) ([]*types.Message, error) { parentTs := ss.head // First we make sure we don't have an upgrade at this epoch. If we do, we return no // messages so we can just create an empty block at that epoch. // // This isn't what the network does, but it makes things easier. Otherwise, we'd need to run // migrations before this epoch and I'd rather not deal with that. nextHeight := parentTs.Height() + 1 prevVer := ss.StateManager.GetNtwkVersion(ctx, nextHeight-1) nextVer := ss.StateManager.GetNtwkVersion(ctx, nextHeight) if nextVer != prevVer { log.Warnw("packing no messages for version upgrade block", "old", prevVer, "new", nextVer, "epoch", nextHeight, ) return nil, nil } // Next, we compute the state for the parent tipset. In practice, this will likely be // cached. parentState, _, err := ss.StateManager.TipSetState(ctx, parentTs) if err != nil { return nil, err } // Then we construct a VM to execute messages for gas estimation. // // Most parts of this VM are "real" except: // 1. We don't charge a fee. // 2. The runtime has "fake" proof logic. // 3. We don't actually save any of the results. r := store.NewChainRand(ss.StateManager.ChainStore(), parentTs.Cids()) vmopt := &vm.VMOpts{ StateBase: parentState, Epoch: nextHeight, Rand: r, Bstore: ss.StateManager.ChainStore().StateBlockstore(), Syscalls: ss.StateManager.ChainStore().VMSys(), CircSupplyCalc: ss.StateManager.GetVMCirculatingSupply, NtwkVersion: ss.StateManager.GetNtwkVersion, BaseFee: baseFee, // FREE! LookbackState: stmgr.LookbackStateGetterForTipset(ss.StateManager, parentTs), } vmi, err := vm.NewVM(ctx, vmopt) if err != nil { return nil, err } // Next we define a helper function for "pushing" messages. This is the function that will // be passed to the "pack" functions. // // It. // // 1. Tries to execute the message on-top-of the already pushed message. // 2. Is careful to revert messages on failure to avoid nasties like nonce-gaps. // 3. Resolves IDs as necessary, fills in missing parts of the message, etc. vmStore := vmi.ActorStore(ctx) var gasTotal int64 var messages []*types.Message tryPushMsg := func(msg *types.Message) (*types.MessageReceipt, error) { if gasTotal >= targetGas { return nil, ErrOutOfGas } // 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 nil, err } msg.Nonce = actor.Nonce if msg.From.Protocol() == address.ID { state, err := account.Load(vmStore, actor) if err != nil { return nil, err } msg.From, err = state.PubkeyAddress() if err != nil { return nil, 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 nil, err } if ret.ActorErr != nil { _ = st.Revert() return nil, ret.ActorErr } // Sometimes there are bugs. Let's catch them. if ret.GasUsed == 0 { _ = st.Revert() return nil, 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 nil, ErrOutOfGas } gasTotal = newTotal // Update the gas limit. msg.GasLimit = ret.GasUsed messages = append(messages, msg) return &ret.MessageReceipt, nil } // Finally, we generate a set of messages to be included in if err := ss.packMessages(ctx, tryPushMsg); err != nil { return nil, err } return messages, nil } // functionName extracts the name of given function. func functionName(fn interface{}) string { name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() lastDot := strings.LastIndexByte(name, '.') if lastDot >= 0 { name = name[lastDot+1 : len(name)-3] } lastDash := strings.LastIndexByte(name, '-') if lastDash > 0 { name = name[:lastDash] } return name } // packMessages packs messages with the given packFunc until the block is full (packFunc returns // true). // TODO: Make this more configurable for other simulations. func (ss *simulationState) packMessages(ctx context.Context, cb packFunc) error { type messageGenerator func(ctx context.Context, cb packFunc) error // We pack messages in-order: // 1. Any window posts. We pack window posts as soon as the deadline opens to ensure we only // miss them if/when we run out of chain bandwidth. // 2. We then move funds to our "funding" account, if it's running low. // 3. Prove commits. We do this eagerly to ensure they don't expire. // 4. Finally, we fill the rest of the space with pre-commits. messageGenerators := []messageGenerator{ ss.packWindowPoSts, ss.packFunding, ss.packProveCommits, ss.packPreCommits, } for _, mgen := range messageGenerators { // We're intentionally ignoring the "full" signal so we can try to pack a few more // messages. if err := mgen(ctx, cb); err != nil && !xerrors.Is(err, ErrOutOfGas) { return xerrors.Errorf("when packing messages with %s: %w", functionName(mgen), err) } } return nil }