Skip to content

Commit 6067968

Browse files
authored
Merge pull request #9654 from filecoin-project/gstuart/gas-estimation-tooling
feat: cli: gas estimation tooling
2 parents 0bb7996 + 9603500 commit 6067968

File tree

9 files changed

+444
-208
lines changed

9 files changed

+444
-208
lines changed

build/drand.go

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ var DrandConfigs = map[DrandEnum]dtypes.DrandConfig{
6969
ChainInfoJSON: `{"public_key":"8cda589f88914aa728fd183f383980b35789ce81b274e5daee1f338b77d02566ef4d3fb0098af1f844f10f9c803c1827","period":25,"genesis_time":1595348225,"hash":"e73b7dc3c4f6a236378220c0dd6aa110eb16eed26c11259606e07ee122838d4f","groupHash":"567d4785122a5a3e75a9bc9911d7ea807dd85ff76b78dc4ff06b075712898607"}`,
7070
},
7171
DrandIncentinet: {
72+
Servers: []string{
73+
"https://dev1.drand.sh",
74+
"https://dev2.drand.sh",
75+
},
7276
ChainInfoJSON: `{"public_key":"8cad0c72c606ab27d36ee06de1d5b2db1faf92e447025ca37575ab3a8aac2eaae83192f846fc9e158bc738423753d000","period":30,"genesis_time":1595873820,"hash":"80c8b872c714f4c00fdd3daa465d5514049f457f01f85a4caf68cdcd394ba039","groupHash":"d9406aaed487f7af71851b4399448e311f2328923d454e971536c05398ce2d9b"}`,
7377
},
7478
}

chain/beacon/drand/drand.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func NewDrandBeacon(genesisTs, interval uint64, ps *pubsub.PubSub, config dtypes
107107

108108
client, err := dclient.Wrap(clients, opts...)
109109
if err != nil {
110-
return nil, xerrors.Errorf("creating drand client")
110+
return nil, xerrors.Errorf("creating drand client: %w", err)
111111
}
112112

113113
lc, err := lru.New(1024)

chain/stmgr/call.go

+79-145
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/filecoin-project/go-state-types/abi"
1515
"github.com/filecoin-project/go-state-types/big"
1616
"github.com/filecoin-project/go-state-types/crypto"
17+
"github.com/filecoin-project/go-state-types/network"
1718

1819
"github.com/filecoin-project/lotus/api"
1920
"github.com/filecoin-project/lotus/blockstore"
@@ -29,74 +30,7 @@ var ErrExpensiveFork = errors.New("refusing explicit call due to state fork at e
2930
// Call applies the given message to the given tipset's parent state, at the epoch following the
3031
// tipset's parent. In the presence of null blocks, the height at which the message is invoked may
3132
// be less than the specified tipset.
32-
//
33-
// - If no tipset is specified, the first tipset without an expensive migration is used.
34-
// - If executing a message at a given tipset would trigger an expensive migration, the call will
35-
// fail with ErrExpensiveFork.
3633
func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) {
37-
ctx, span := trace.StartSpan(ctx, "statemanager.Call")
38-
defer span.End()
39-
40-
var pheight abi.ChainEpoch = -1
41-
42-
// If no tipset is provided, try to find one without a fork.
43-
if ts == nil {
44-
ts = sm.cs.GetHeaviestTipSet()
45-
// Search back till we find a height with no fork, or we reach the beginning.
46-
for ts.Height() > 0 {
47-
pts, err := sm.cs.GetTipSetFromKey(ctx, ts.Parents())
48-
if err != nil {
49-
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
50-
}
51-
if !sm.hasExpensiveFork(pts.Height()) {
52-
pheight = pts.Height()
53-
break
54-
}
55-
ts = pts
56-
}
57-
} else if ts.Height() > 0 {
58-
pts, err := sm.cs.LoadTipSet(ctx, ts.Parents())
59-
if err != nil {
60-
return nil, xerrors.Errorf("failed to load parent tipset: %w", err)
61-
}
62-
pheight = pts.Height()
63-
if sm.hasExpensiveFork(pheight) {
64-
return nil, ErrExpensiveFork
65-
}
66-
} else {
67-
// We can't get the parent tipset in this case.
68-
pheight = ts.Height() - 1
69-
}
70-
71-
// Since we're simulating a future message, pretend we're applying it in the "next" tipset
72-
vmHeight := pheight + 1
73-
bstate := ts.ParentState()
74-
75-
// Run the (not expensive) migration.
76-
bstate, err := sm.HandleStateForks(ctx, bstate, pheight, nil, ts)
77-
if err != nil {
78-
return nil, fmt.Errorf("failed to handle fork: %w", err)
79-
}
80-
81-
vmopt := &vm.VMOpts{
82-
StateBase: bstate,
83-
Epoch: vmHeight,
84-
Rand: rand.NewStateRand(sm.cs, ts.Cids(), sm.beacon, sm.GetNetworkVersion),
85-
Bstore: sm.cs.StateBlockstore(),
86-
Actors: sm.tsExec.NewActorRegistry(),
87-
Syscalls: sm.Syscalls,
88-
CircSupplyCalc: sm.GetVMCirculatingSupply,
89-
NetworkVersion: sm.GetNetworkVersion(ctx, pheight+1),
90-
BaseFee: types.NewInt(0),
91-
LookbackState: LookbackStateGetterForTipset(sm, ts),
92-
Tracing: true,
93-
}
94-
95-
vmi, err := sm.newVM(ctx, vmopt)
96-
if err != nil {
97-
return nil, xerrors.Errorf("failed to set up vm: %w", err)
98-
}
99-
10034
if msg.GasLimit == 0 {
10135
msg.GasLimit = build.BlockGasLimit
10236
}
@@ -106,61 +40,43 @@ func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.
10640
if msg.GasPremium == types.EmptyInt {
10741
msg.GasPremium = types.NewInt(0)
10842
}
109-
11043
if msg.Value == types.EmptyInt {
11144
msg.Value = types.NewInt(0)
11245
}
11346

114-
if span.IsRecordingEvents() {
115-
span.AddAttributes(
116-
trace.Int64Attribute("gas_limit", msg.GasLimit),
117-
trace.StringAttribute("gas_feecap", msg.GasFeeCap.String()),
118-
trace.StringAttribute("value", msg.Value.String()),
119-
)
120-
}
121-
122-
stTree, err := sm.StateTree(bstate)
123-
if err != nil {
124-
return nil, xerrors.Errorf("failed to load state tree: %w", err)
125-
}
126-
127-
fromActor, err := stTree.GetActor(msg.From)
128-
if err != nil {
129-
return nil, xerrors.Errorf("call raw get actor: %s", err)
130-
}
131-
132-
msg.Nonce = fromActor.Nonce
47+
return sm.callInternal(ctx, msg, nil, ts, cid.Undef, sm.GetNetworkVersion, false)
48+
}
13349

134-
// TODO: maybe just use the invoker directly?
135-
ret, err := vmi.ApplyImplicitMessage(ctx, msg)
136-
if err != nil && ret == nil {
137-
return nil, xerrors.Errorf("apply message failed: %w", err)
138-
}
50+
// CallWithGas calculates the state for a given tipset, and then applies the given message on top of that state.
51+
func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet) (*api.InvocResult, error) {
52+
return sm.callInternal(ctx, msg, priorMsgs, ts, cid.Undef, sm.GetNetworkVersion, true)
53+
}
13954

140-
var errs string
141-
if ret.ActorErr != nil {
142-
errs = ret.ActorErr.Error()
143-
log.Warnf("chain call failed: %s", ret.ActorErr)
55+
// CallAtStateAndVersion allows you to specify a message to execute on the given stateCid and network version.
56+
// This should mostly be used for gas modelling on a migrated state.
57+
// Tipset here is not needed because stateCid and network version fully describe execution we want. The internal function
58+
// will get the heaviest tipset for use for things like basefee, which we don't really care about here.
59+
func (sm *StateManager) CallAtStateAndVersion(ctx context.Context, msg *types.Message, stateCid cid.Cid, v network.Version) (*api.InvocResult, error) {
60+
nvGetter := func(context.Context, abi.ChainEpoch) network.Version {
61+
return v
14462
}
14563

146-
return &api.InvocResult{
147-
MsgCid: msg.Cid(),
148-
Msg: msg,
149-
MsgRct: &ret.MessageReceipt,
150-
ExecutionTrace: ret.ExecutionTrace,
151-
Error: errs,
152-
Duration: ret.Duration,
153-
}, err
64+
return sm.callInternal(ctx, msg, nil, nil, stateCid, nvGetter, true)
15465
}
15566

156-
func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet) (*api.InvocResult, error) {
157-
ctx, span := trace.StartSpan(ctx, "statemanager.CallWithGas")
67+
// - If no tipset is specified, the first tipset without an expensive migration or one in its parent is used.
68+
// - If executing a message at a given tipset or its parent would trigger an expensive migration, the call will
69+
// fail with ErrExpensiveFork.
70+
func (sm *StateManager) callInternal(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, stateCid cid.Cid, nvGetter rand.NetworkVersionGetter, checkGas bool) (*api.InvocResult, error) {
71+
ctx, span := trace.StartSpan(ctx, "statemanager.callInternal")
15872
defer span.End()
15973

16074
// Copy the message as we'll be modifying the nonce.
16175
msgCopy := *msg
16276
msg = &msgCopy
16377

78+
var err error
79+
var pts *types.TipSet
16480
if ts == nil {
16581
ts = sm.cs.GetHeaviestTipSet()
16682

@@ -170,18 +86,19 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
17086
// height to have no fork, because we'll run it inside this
17187
// function before executing the given message.
17288
for ts.Height() > 0 {
173-
pts, err := sm.cs.GetTipSetFromKey(ctx, ts.Parents())
89+
pts, err = sm.cs.GetTipSetFromKey(ctx, ts.Parents())
17490
if err != nil {
17591
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
17692
}
93+
// Checks for expensive forks from the parents to the tipset, including nil tipsets
17794
if !sm.hasExpensiveForkBetween(pts.Height(), ts.Height()+1) {
17895
break
17996
}
18097

18198
ts = pts
18299
}
183100
} else if ts.Height() > 0 {
184-
pts, err := sm.cs.GetTipSetFromKey(ctx, ts.Parents())
101+
pts, err = sm.cs.GetTipSetFromKey(ctx, ts.Parents())
185102
if err != nil {
186103
return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err)
187104
}
@@ -190,12 +107,22 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
190107
}
191108
}
192109

193-
// Since we're simulating a future message, pretend we're applying it in the "next" tipset
194-
vmHeight := ts.Height() + 1
195-
196-
stateCid, _, err := sm.TipSetState(ctx, ts)
197-
if err != nil {
198-
return nil, xerrors.Errorf("computing tipset state: %w", err)
110+
var vmHeight abi.ChainEpoch
111+
if checkGas {
112+
// Since we're simulating a future message, pretend we're applying it in the "next" tipset
113+
vmHeight = ts.Height() + 1
114+
if stateCid == cid.Undef {
115+
stateCid, _, err = sm.TipSetState(ctx, ts)
116+
if err != nil {
117+
return nil, xerrors.Errorf("computing tipset state: %w", err)
118+
}
119+
}
120+
} else {
121+
// If we're not checking gas, we don't want to have to execute the tipset like above. This saves a lot of computation time
122+
vmHeight = pts.Height() + 1
123+
if stateCid == cid.Undef {
124+
stateCid = ts.ParentState()
125+
}
199126
}
200127

201128
// Technically, the tipset we're passing in here should be ts+1, but that may not exist.
@@ -204,8 +131,6 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
204131
return nil, fmt.Errorf("failed to handle fork: %w", err)
205132
}
206133

207-
r := rand.NewStateRand(sm.cs, ts.Cids(), sm.beacon, sm.GetNetworkVersion)
208-
209134
if span.IsRecordingEvents() {
210135
span.AddAttributes(
211136
trace.Int64Attribute("gas_limit", msg.GasLimit),
@@ -218,12 +143,12 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
218143
vmopt := &vm.VMOpts{
219144
StateBase: stateCid,
220145
Epoch: vmHeight,
221-
Rand: r,
146+
Rand: rand.NewStateRand(sm.cs, ts.Cids(), sm.beacon, nvGetter),
222147
Bstore: buffStore,
223148
Actors: sm.tsExec.NewActorRegistry(),
224149
Syscalls: sm.Syscalls,
225150
CircSupplyCalc: sm.GetVMCirculatingSupply,
226-
NetworkVersion: sm.GetNetworkVersion(ctx, ts.Height()+1),
151+
NetworkVersion: nvGetter(ctx, vmHeight),
227152
BaseFee: ts.Blocks()[0].ParentBaseFee,
228153
LookbackState: LookbackStateGetterForTipset(sm, ts),
229154
Tracing: true,
@@ -233,7 +158,7 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
233158
return nil, xerrors.Errorf("failed to set up vm: %w", err)
234159
}
235160
for i, m := range priorMsgs {
236-
_, err := vmi.ApplyMessage(ctx, m)
161+
_, err = vmi.ApplyMessage(ctx, m)
237162
if err != nil {
238163
return nil, xerrors.Errorf("applying prior message (%d, %s): %w", i, m.Cid(), err)
239164
}
@@ -258,27 +183,6 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
258183

259184
msg.Nonce = fromActor.Nonce
260185

261-
fromKey, err := sm.ResolveToKeyAddress(ctx, msg.From, ts)
262-
if err != nil {
263-
return nil, xerrors.Errorf("could not resolve key: %w", err)
264-
}
265-
266-
var msgApply types.ChainMsg
267-
268-
switch fromKey.Protocol() {
269-
case address.BLS:
270-
msgApply = msg
271-
case address.SECP256K1:
272-
msgApply = &types.SignedMessage{
273-
Message: *msg,
274-
Signature: crypto.Signature{
275-
Type: crypto.SigTypeSecp256k1,
276-
Data: make([]byte, 65),
277-
},
278-
}
279-
280-
}
281-
282186
// If the fee cap is set to zero, make gas free.
283187
if msg.GasFeeCap.NilOrZero() {
284188
// Now estimate with a new VM with no base fee.
@@ -291,9 +195,39 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
291195
}
292196
}
293197

294-
ret, err := vmi.ApplyMessage(ctx, msgApply)
295-
if err != nil {
296-
return nil, xerrors.Errorf("gas estimation failed: %w", err)
198+
var ret *vm.ApplyRet
199+
var gasInfo api.MsgGasCost
200+
if checkGas {
201+
fromKey, err := sm.ResolveToKeyAddress(ctx, msg.From, ts)
202+
if err != nil {
203+
return nil, xerrors.Errorf("could not resolve key: %w", err)
204+
}
205+
206+
var msgApply types.ChainMsg
207+
208+
switch fromKey.Protocol() {
209+
case address.BLS:
210+
msgApply = msg
211+
case address.SECP256K1:
212+
msgApply = &types.SignedMessage{
213+
Message: *msg,
214+
Signature: crypto.Signature{
215+
Type: crypto.SigTypeSecp256k1,
216+
Data: make([]byte, 65),
217+
},
218+
}
219+
}
220+
221+
ret, err = vmi.ApplyMessage(ctx, msgApply)
222+
if err != nil {
223+
return nil, xerrors.Errorf("gas estimation failed: %w", err)
224+
}
225+
gasInfo = MakeMsgGasCost(msg, ret)
226+
} else {
227+
ret, err = vmi.ApplyImplicitMessage(ctx, msg)
228+
if err != nil && ret == nil {
229+
return nil, xerrors.Errorf("apply message failed: %w", err)
230+
}
297231
}
298232

299233
var errs string
@@ -305,11 +239,11 @@ func (sm *StateManager) CallWithGas(ctx context.Context, msg *types.Message, pri
305239
MsgCid: msg.Cid(),
306240
Msg: msg,
307241
MsgRct: &ret.MessageReceipt,
308-
GasCost: MakeMsgGasCost(msg, ret),
242+
GasCost: gasInfo,
309243
ExecutionTrace: ret.ExecutionTrace,
310244
Error: errs,
311245
Duration: ret.Duration,
312-
}, nil
246+
}, err
313247
}
314248

315249
var errHaltExecution = fmt.Errorf("halt")

chain/stmgr/forks_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func testForkRefuseCall(t *testing.T, nullsBefore, nullsAfter int) {
335335
parentHeight := pts.Height()
336336
currentHeight := ts.TipSet.TipSet().Height()
337337

338-
// CallWithGas calls _at_ the current tipset.
338+
// CallWithGas calls on top of the given tipset.
339339
ret, err := sm.CallWithGas(ctx, m, nil, ts.TipSet.TipSet())
340340
if parentHeight <= testForkHeight && currentHeight >= testForkHeight {
341341
// If I had a fork, or I _will_ have a fork, it should fail.
@@ -347,7 +347,7 @@ func testForkRefuseCall(t *testing.T, nullsBefore, nullsAfter int) {
347347

348348
// Call always applies the message to the "next block" after the tipset's parent state.
349349
ret, err = sm.Call(ctx, m, ts.TipSet.TipSet())
350-
if parentHeight == testForkHeight {
350+
if parentHeight <= testForkHeight && currentHeight >= testForkHeight {
351351
require.Equal(t, ErrExpensiveFork, err)
352352
} else {
353353
require.NoError(t, err)

chain/types/execresult.go

+19
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ type Loc struct {
4242
Function string
4343
}
4444

45+
func (et ExecutionTrace) SumGas() GasTrace {
46+
return SumGas(et.GasCharges)
47+
}
48+
49+
func SumGas(charges []*GasTrace) GasTrace {
50+
var out GasTrace
51+
for _, gc := range charges {
52+
out.TotalGas += gc.TotalGas
53+
out.ComputeGas += gc.ComputeGas
54+
out.StorageGas += gc.StorageGas
55+
56+
out.TotalVirtualGas += gc.TotalVirtualGas
57+
out.VirtualComputeGas += gc.VirtualComputeGas
58+
out.VirtualStorageGas += gc.VirtualStorageGas
59+
}
60+
61+
return out
62+
}
63+
4564
func (l Loc) Show() bool {
4665
ignorePrefix := []string{
4766
"reflect.",

0 commit comments

Comments
 (0)