-
Notifications
You must be signed in to change notification settings - Fork 170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Gas accounting #676
WIP: Gas accounting #676
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,14 +33,14 @@ func (vmi *VMInterpreter_I) ApplyTipSetMessages(inTree st.StateTree, msgs TipSet | |
for _, blk := range msgs.Blocks() { | ||
// Pay block reward. | ||
reward := _makeBlockRewardMessage(outTree, blk.Miner()) | ||
outTree, r = vmi.ApplyMessage(outTree, reward, blk.Miner()) | ||
outTree, r = vmi.ApplyMessage(outTree, reward, 0, blk.Miner()) | ||
if r.ExitCode() != exitcode.OK() { | ||
panic("block reward failed") | ||
} | ||
|
||
// Process block miner's Election PoSt. | ||
epost := _makeElectionPoStMessage(outTree, blk.Miner(), msgs.Epoch(), blk.PoStProof()) | ||
outTree, r = vmi.ApplyMessage(outTree, epost, blk.Miner()) | ||
outTree, r = vmi.ApplyMessage(outTree, epost, 0, blk.Miner()) | ||
if r.ExitCode() != exitcode.OK() { | ||
panic("election post failed") | ||
} | ||
|
@@ -51,71 +51,129 @@ func (vmi *VMInterpreter_I) ApplyTipSetMessages(inTree st.StateTree, msgs TipSet | |
if found { | ||
continue | ||
} | ||
outTree, r = vmi.ApplyMessage(outTree, m, blk.Miner()) | ||
|
||
TODO() // TODO: include signature length? | ||
onChainMessageLen := len(msg.Serialize_UnsignedMessage(m)) | ||
|
||
outTree, r = vmi.ApplyMessage(outTree, m, onChainMessageLen, blk.Miner()) | ||
receipts = append(receipts, r) | ||
seenMsgs[_msgCID(m)] = struct{}{} | ||
} | ||
} | ||
|
||
// Invoke cron tick, attributing it to the miner of the first block. | ||
|
||
TODO() | ||
// TODO: miners shouldn't be able to trigger cron by sending messages. | ||
// Maybe we need a separate ControlActor for this? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This TODO isn't helpful here, please remove it. It belongs in the cron actor and we already have an issue for it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TODO isn't intended to remain in the code; it's a placeholder to signal that review is needed (and also serves to ensure that we don't accidentally leave the code incomplete). I can replace the question with a link to the issue, though, if helpful. |
||
|
||
cronSender := msgs.Blocks()[0].Miner() | ||
cron := _makeCronTickMessage(outTree, cronSender) | ||
outTree, r = vmi.ApplyMessage(outTree, cron, cronSender) | ||
outTree, r = vmi.ApplyMessage(outTree, cron, 0, cronSender) | ||
if r.ExitCode() != exitcode.OK() { | ||
panic("cron tick failed") | ||
} | ||
|
||
return | ||
} | ||
|
||
func (vmi *VMInterpreter_I) ApplyMessage(inTree st.StateTree, message msg.UnsignedMessage, minerAddr addr.Address) ( | ||
func StateTree_WithGasUsedFundsTransfer( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this needn't be exported, so change to |
||
tree st.StateTree, gasUsed msg.GasAmount, message msg.UnsignedMessage, | ||
fundsFrom addr.Address, fundsTo addr.Address) st.StateTree { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
TODO() // TODO: what if the miner has insufficient funds? | ||
// Should we instead aggregate these deltas, and SubtractWhileNonnegative at the end of the tipset? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In almost all cases the miner is receiving funds, so this is confusing. Do you mean sender? This may not fail in any situation, so cap the funds to be transferred at the sender's balance at some point. That probably means loading the miner actor's state (as well as the sender's). If you think this is insufficient, let's discuss approaches elsewhere. The prose already written explicitly answers the final question: the desired semantics are immediate availability. |
||
|
||
return _withTransferFundsAssert( | ||
tree, | ||
fundsFrom, | ||
fundsTo, | ||
_gasToFIL(gasUsed, message.GasPrice()), | ||
) | ||
} | ||
|
||
func (vmi *VMInterpreter_I) ApplyMessage( | ||
inTree st.StateTree, message msg.UnsignedMessage, onChainMessageSize int, minerAddr addr.Address) ( | ||
st.StateTree, msg.MessageReceipt) { | ||
|
||
compTree := inTree | ||
var outTree st.StateTree | ||
var toActor actor.ActorState | ||
var err error | ||
vmiGasRemaining := message.GasLimit() | ||
|
||
_applyReturn := func( | ||
tree st.StateTree, invocOutput msg.InvocOutput, exitCode exitcode.ExitCode, | ||
gasUsedFrom addr.Address, gasUsedTo addr.Address) (st.StateTree, msg.MessageReceipt) { | ||
|
||
vmiGasUsed := message.GasLimit().Subtract(vmiGasRemaining) | ||
tree = StateTree_WithGasUsedFundsTransfer(tree, vmiGasUsed, message, gasUsedFrom, gasUsedTo) | ||
return tree, msg.MessageReceipt_Make(invocOutput, exitCode, vmiGasUsed) | ||
} | ||
|
||
fromActor := compTree.GetActorState(message.From()) | ||
_applyError := func( | ||
tree st.StateTree, errExitCode exitcode.SystemErrorCode, | ||
gasUsedFrom addr.Address, gasUsedTo addr.Address) (st.StateTree, msg.MessageReceipt) { | ||
|
||
return _applyReturn( | ||
tree, msg.InvocOutput_Make(nil), exitcode.SystemError(errExitCode), gasUsedFrom, gasUsedTo) | ||
} | ||
|
||
_vmiUseGas := func(amount msg.GasAmount) (vmiUseGasOK bool) { | ||
vmiGasRemaining, vmiUseGasOK = vmiGasRemaining.SubtractWhileNonnegative(amount) | ||
return | ||
} | ||
|
||
ok := _vmiUseGas(gascost.OnChainMessage(onChainMessageSize)) | ||
if !ok { | ||
return _applyError(inTree, exitcode.OutOfGas, minerAddr, addr.BurntFundsActorAddr) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If you don't like the idea that a message could go on chain without paying at least for its weight, then we can add a message syntactic validation rule for a minimum gas limit (and then treat the issue like any other out-of-gas here). But I don't think we need to: the miner is forgoing the gas reward. |
||
} | ||
|
||
fromActor := inTree.GetActorState(message.From()) | ||
if fromActor == nil { | ||
// TODO: This was originally exitcode.InvalidMethod; which is correct? | ||
return inTree, _applyError(exitcode.ActorNotFound) | ||
return _applyError(inTree, exitcode.ActorNotFound, minerAddr, addr.BurntFundsActorAddr) | ||
} | ||
|
||
// make sure fromActor has enough money to run the max invocation | ||
maxGasCost := _gasToFIL(message.GasLimit(), message.GasPrice()) | ||
totalCost := message.Value() + actor.TokenAmount(maxGasCost) | ||
// make sure fromActor has enough money to run with the specified gas limit | ||
gasLimitCost := _gasToFIL(message.GasLimit(), message.GasPrice()) | ||
totalCost := message.Value() + actor.TokenAmount(gasLimitCost) | ||
if fromActor.Balance() < totalCost { | ||
return inTree, _applyError(exitcode.InsufficientFunds_System) | ||
return _applyError(inTree, exitcode.InsufficientFunds_System, minerAddr, addr.BurntFundsActorAddr) | ||
} | ||
|
||
// make sure this is the right message order for fromActor | ||
// (this is protection against replay attacks, and useful sequencing) | ||
if message.CallSeqNum() != fromActor.CallSeqNum()+1 { | ||
return inTree, _applyError(exitcode.InvalidCallSeqNum) | ||
return _applyError(inTree, exitcode.InvalidCallSeqNum, minerAddr, addr.BurntFundsActorAddr) | ||
} | ||
|
||
// WithActorForAddress may create new account actors | ||
compTree, toActor = compTree.Impl().WithActorForAddress(message.To()) | ||
compTreePreSend := inTree | ||
compTreePreSend, toActor := compTreePreSend.Impl().WithActorForAddress(message.To()) | ||
if toActor == nil { | ||
return inTree, _applyError(exitcode.ActorNotFound) | ||
return _applyError(inTree, exitcode.ActorNotFound, message.From(), addr.BurntFundsActorAddr) | ||
} | ||
|
||
// deduct maximum expenditure gas funds first | ||
compTree = _withTransferFundsAssert(compTree, message.From(), addr.BurntFundsActorAddr, maxGasCost) | ||
// deduct gas limit funds from sender first | ||
compTreePreSend = _withTransferFundsAssert( | ||
compTreePreSend, message.From(), addr.BurntFundsActorAddr, gasLimitCost) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it would be more clear why this assertion is ok if you moved it up directly below where the balance is checked. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately I think this would cause more confusion due to the out-of-order state tree threading. But I'll add a comment explaining why the assertion is OK. |
||
|
||
// Increment sender call sequence number. | ||
var err error | ||
compTreePreSend, err = compTreePreSend.Impl().WithIncrementedCallSeqNum(message.From()) | ||
if err != nil { | ||
// Note: if actor deletion is possible at some point, may need to allow this case | ||
panic("Internal interpreter error: failed to increment call sequence number") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not now. I recommend removing the error return from WithIncrementedCallSeqNum(). That call can panic instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK -- will modify the API (WithIncrementedCallSeqNum / WithIncrementedCallSeqNum_Assert). |
||
|
||
rt := vmr.VMContext_Make( | ||
message.From(), | ||
minerAddr, // TODO: may not exist? (see below) | ||
minerAddr, | ||
fromActor.CallSeqNum(), | ||
actor.CallSeqNum(0), | ||
compTree, | ||
compTreePreSend, | ||
message.From(), | ||
actor.TokenAmount(0), | ||
message.GasLimit(), | ||
vmiGasRemaining, | ||
) | ||
|
||
sendRet, sendRetStateTree := rt.SendToplevelFromInterpreter( | ||
sendRet, compTreePostSend := rt.SendToplevelFromInterpreter( | ||
msg.InvocInput_Make( | ||
message.To(), | ||
message.Method(), | ||
|
@@ -124,51 +182,33 @@ func (vmi *VMInterpreter_I) ApplyMessage(inTree st.StateTree, message msg.Unsign | |
), | ||
) | ||
|
||
if !sendRet.ExitCode().AllowsStateUpdate() { | ||
// error -- revert all state changes -- ie drop updates. burn used gas. | ||
outTree = inTree | ||
outTree = _withTransferFundsAssert( | ||
outTree, | ||
message.From(), | ||
addr.BurntFundsActorAddr, | ||
_gasToFIL(sendRet.GasUsed(), message.GasPrice()), | ||
) | ||
} else { | ||
// success -- refund unused gas | ||
outTree = sendRetStateTree | ||
refundGas := message.GasLimit() - sendRet.GasUsed() | ||
TODO() // TODO: assert refundGas is nonnegative | ||
outTree = _withTransferFundsAssert( | ||
outTree, | ||
addr.BurntFundsActorAddr, | ||
message.From(), | ||
_gasToFIL(refundGas, message.GasPrice()), | ||
) | ||
ok = _vmiUseGas(sendRet.GasUsed()) | ||
if !ok { | ||
panic("Interpreter error: runtime execution used more gas than provided") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a panic, this is just out of gas (exceeded the pre-set limit). Before execution, it's impossible to know how much will be actually consumed. An efficient implementation will of course halt execution before this point, but IMO the spec doesn't need to. In any case, the semantics are well defined: pay all the gas and revert state changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be impossible for the runtime execution to use more gas than provided -- as specced, it uses |
||
} | ||
|
||
outTree, err = outTree.Impl().WithIncrementedCallSeqNum(message.To()) | ||
if err != nil { | ||
// TODO: if actor deletion is possible at some point, may need to allow this case | ||
panic("Internal interpreter error: failed to increment call sequence number") | ||
ok = _vmiUseGas(gascost.OnChainReturnValue(sendRet.ReturnValue())) | ||
if !ok { | ||
return _applyError(compTreePreSend, exitcode.OutOfGas, addr.BurntFundsActorAddr, minerAddr) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just out-of-gas - the caller should pay gas limit and the miner should not be penalized. |
||
} | ||
|
||
compTreeRet := compTreePreSend | ||
if sendRet.ExitCode().AllowsStateUpdate() { | ||
compTreeRet = compTreePostSend | ||
} | ||
|
||
// reward miner gas fees | ||
outTree = _withTransferFundsAssert( | ||
outTree, | ||
// Refund unused gas to sender. | ||
refundGas := vmiGasRemaining | ||
compTreeRet = _withTransferFundsAssert( | ||
compTreeRet, | ||
addr.BurntFundsActorAddr, | ||
minerAddr, // TODO: may not exist | ||
_gasToFIL(sendRet.GasUsed(), message.GasPrice()), | ||
message.From(), | ||
_gasToFIL(refundGas, message.GasPrice()), | ||
) | ||
|
||
return outTree, sendRet | ||
} | ||
|
||
func _applyError(errCode exitcode.SystemErrorCode) msg.MessageReceipt { | ||
// TODO: should this gasUsed value be zero? | ||
// If nonzero, there is not guaranteed to be a nonzero gas balance from which to deduct it. | ||
gasUsed := gascost.ApplyMessageFail | ||
TODO() | ||
return msg.MessageReceipt_MakeSystemError(errCode, gasUsed) | ||
return _applyReturn( | ||
compTreeRet, msg.InvocOutput_Make(sendRet.ReturnValue()), sendRet.ExitCode(), | ||
addr.BurntFundsActorAddr, minerAddr) | ||
} | ||
|
||
func _withTransferFundsAssert(tree st.StateTree, from addr.Address, to addr.Address, amount actor.TokenAmount) st.StateTree { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,11 @@ type TipSetMessages struct { | |
Epoch UInt64 // The chain epoch of the blocks | ||
} | ||
|
||
type MessageRef struct { | ||
Message msg.UnsignedMessage | ||
MessageSizeOrig int | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment in vm_interpreter.go. You could replace the array elements in BlockMessages.Messages with this, but it's probably better to represent the BLS and SECP messages explicitly, so this spec can also describe which order they are evaluated in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (deferring pending #669) |
||
type VMInterpreter struct { | ||
ApplyTipSetMessages(inTree st.StateTree, msgs TipSetMessages) struct {outTree st.StateTree, ret [msg.MessageReceipt]} | ||
ApplyMessage(inTree st.StateTree, msg msg.Message, minerAddr addr.Address) struct {outTree st.StateTree, ret msg.MessageReceipt} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to adjust the structures to pass the serialized size through from the outside. That size includes the signature for secp messages but not for BLS.
Alternatively, if you want to compute that size here, change BlockMessages to have two message lists, BLSMessages (unsigned) and SECPMessages (signed). You'll then need to extract them into a single list for execution, BLS first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(deferring pending resolution of #669)