mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-11 22:43:47 +00:00
Further work towards having a compatible Firehose code (at battlefied block #15)
This commit is contained in:
parent
d723124530
commit
c5eb5faa5f
2 changed files with 128 additions and 37 deletions
|
|
@ -30,6 +30,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var isFirehoseDebugEnabled = os.Getenv("GETH_FIREHOSE_TRACER_DEBUG") != ""
|
var isFirehoseDebugEnabled = os.Getenv("GETH_FIREHOSE_TRACER_DEBUG") != ""
|
||||||
|
var isFirehoseTracerEnabled = os.Getenv("GETH_FIREHOSE_TRACER_TRACE") != ""
|
||||||
|
|
||||||
var _ core.BlockchainLogger = (*Firehose)(nil)
|
var _ core.BlockchainLogger = (*Firehose)(nil)
|
||||||
|
|
||||||
|
|
@ -51,6 +52,7 @@ type Firehose struct {
|
||||||
inTransaction *atomic.Bool
|
inTransaction *atomic.Bool
|
||||||
transaction *pbeth.TransactionTrace
|
transaction *pbeth.TransactionTrace
|
||||||
transactionLogIndex uint32
|
transactionLogIndex uint32
|
||||||
|
isPrecompiledAddr func(addr common.Address) bool
|
||||||
|
|
||||||
// Call state
|
// Call state
|
||||||
callStack *CallStack
|
callStack *CallStack
|
||||||
|
|
@ -73,8 +75,8 @@ func NewFirehoseLogger() *Firehose {
|
||||||
inTransaction: atomic.NewBool(false),
|
inTransaction: atomic.NewBool(false),
|
||||||
transactionLogIndex: 0,
|
transactionLogIndex: 0,
|
||||||
|
|
||||||
callStack: &CallStack{},
|
callStack: NewCallStack(),
|
||||||
deferredCallState: &DeferredCallState{},
|
deferredCallState: NewDeferredCallState(),
|
||||||
latestCallStartSuicided: false,
|
latestCallStartSuicided: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +94,7 @@ func (f *Firehose) resetBlock() {
|
||||||
func (f *Firehose) resetTransaction() {
|
func (f *Firehose) resetTransaction() {
|
||||||
f.inTransaction.Store(false)
|
f.inTransaction.Store(false)
|
||||||
f.transactionLogIndex = 0
|
f.transactionLogIndex = 0
|
||||||
|
f.isPrecompiledAddr = nil
|
||||||
|
|
||||||
f.callStack.Reset()
|
f.callStack.Reset()
|
||||||
f.latestCallStartSuicided = false
|
f.latestCallStartSuicided = false
|
||||||
|
|
@ -137,13 +140,15 @@ func (f *Firehose) OnBlockEnd(err error) {
|
||||||
firehoseDebug("block end")
|
firehoseDebug("block end")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firehose) CaptureTxStart(env *vm.EVM, tx *types.Transaction) {
|
func (f *Firehose) CaptureTxStart(evm *vm.EVM, tx *types.Transaction) {
|
||||||
firehoseDebug("trx start hash=%s type=%d input=%s", tx.Hash(), tx.Type(), inputView(tx.Data()))
|
firehoseDebug("trx start hash=%s type=%d gas=%d input=%s", tx.Hash(), tx.Type(), tx.Gas(), inputView(tx.Data()))
|
||||||
|
|
||||||
f.ensureInBlockAndNotInTrxAndNotInCall()
|
f.ensureInBlockAndNotInTrxAndNotInCall()
|
||||||
|
|
||||||
f.inTransaction.Store(true)
|
f.inTransaction.Store(true)
|
||||||
|
f.isPrecompiledAddr = evm.IsPrecompileAddr
|
||||||
|
|
||||||
signer := types.MakeSigner(env.ChainConfig(), env.Context.BlockNumber, env.Context.Time)
|
signer := types.MakeSigner(evm.ChainConfig(), evm.Context.BlockNumber, evm.Context.Time)
|
||||||
|
|
||||||
from, err := types.Sender(signer, tx)
|
from, err := types.Sender(signer, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -152,7 +157,7 @@ func (f *Firehose) CaptureTxStart(env *vm.EVM, tx *types.Transaction) {
|
||||||
|
|
||||||
var to common.Address
|
var to common.Address
|
||||||
if tx.To() == nil {
|
if tx.To() == nil {
|
||||||
to = crypto.CreateAddress(from, env.StateDB.GetNonce(from))
|
to = crypto.CreateAddress(from, evm.StateDB.GetNonce(from))
|
||||||
} else {
|
} else {
|
||||||
to = *tx.To()
|
to = *tx.To()
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +190,8 @@ func (f *Firehose) CaptureTxEnd(receipt *types.Receipt, err error) {
|
||||||
|
|
||||||
f.block.TransactionTraces = append(f.block.TransactionTraces, f.completeTransaction(receipt))
|
f.block.TransactionTraces = append(f.block.TransactionTraces, f.completeTransaction(receipt))
|
||||||
|
|
||||||
|
// The reset must be done as the very last thing as the CallStack needs to be
|
||||||
|
// properly populated for the `completeTransaction` call above to complete correctly.
|
||||||
f.resetTransaction()
|
f.resetTransaction()
|
||||||
|
|
||||||
firehoseDebug("trx end")
|
firehoseDebug("trx end")
|
||||||
|
|
@ -193,12 +200,15 @@ func (f *Firehose) CaptureTxEnd(receipt *types.Receipt, err error) {
|
||||||
func (f *Firehose) completeTransaction(receipt *types.Receipt) *pbeth.TransactionTrace {
|
func (f *Firehose) completeTransaction(receipt *types.Receipt) *pbeth.TransactionTrace {
|
||||||
firehoseDebug("completing transaction call_count=%d receipt=%s", len(f.transaction.Calls), (*receiptView)(receipt))
|
firehoseDebug("completing transaction call_count=%d receipt=%s", len(f.transaction.Calls), (*receiptView)(receipt))
|
||||||
|
|
||||||
|
// Sorting needs to happen first, before we populate the state reverted
|
||||||
slices.SortFunc(f.transaction.Calls, func(i, j *pbeth.Call) bool {
|
slices.SortFunc(f.transaction.Calls, func(i, j *pbeth.Call) bool {
|
||||||
return i.Index < j.Index
|
return i.Index < j.Index
|
||||||
})
|
})
|
||||||
|
|
||||||
|
rootCall := f.transaction.Calls[0]
|
||||||
|
|
||||||
if !f.deferredCallState.IsEmpty() {
|
if !f.deferredCallState.IsEmpty() {
|
||||||
f.deferredCallState.MaybePopulateCallAndReset("root", f.transaction.Calls[0])
|
f.deferredCallState.MaybePopulateCallAndReset("root", rootCall)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receipt can be nil if an error occurred during the transaction execution, right now we don't have it
|
// Receipt can be nil if an error occurred during the transaction execution, right now we don't have it
|
||||||
|
|
@ -207,21 +217,58 @@ func (f *Firehose) completeTransaction(receipt *types.Receipt) *pbeth.Transactio
|
||||||
f.transaction.GasUsed = receipt.GasUsed
|
f.transaction.GasUsed = receipt.GasUsed
|
||||||
f.transaction.Receipt = newTxReceiptFromChain(receipt)
|
f.transaction.Receipt = newTxReceiptFromChain(receipt)
|
||||||
f.transaction.Status = transactionStatusFromChainTxReceipt(receipt.Status)
|
f.transaction.Status = transactionStatusFromChainTxReceipt(receipt.Status)
|
||||||
} else {
|
|
||||||
// FIXME: How are we going to decide about a reverted transaction?
|
|
||||||
f.transaction.Status = pbeth.TransactionTraceStatus_FAILED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: There should always be a Call in a transaction, maybe we need to synthetize one here, need to check
|
// It's possible that the transaction was reverted, but we still have a receipt, in that case, we must
|
||||||
if len(f.transaction.Calls) > 0 {
|
// check the root call
|
||||||
f.transaction.ReturnData = f.transaction.Calls[0].ReturnData
|
if rootCall.StatusReverted {
|
||||||
|
f.transaction.Status = pbeth.TransactionTraceStatus_REVERTED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order is important, we must populate the state reverted before we remove the log block index
|
||||||
|
f.populateStateReverted()
|
||||||
|
f.removeLogBlockIndexOnStateRevertedCalls()
|
||||||
|
|
||||||
|
// I think this was never used in Firehose instrumentation actually
|
||||||
|
// f.transaction.ReturnData = rootCall.ReturnData
|
||||||
f.transaction.EndOrdinal = f.blockOrdinal.Next()
|
f.transaction.EndOrdinal = f.blockOrdinal.Next()
|
||||||
|
|
||||||
return f.transaction
|
return f.transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Firehose) populateStateReverted() {
|
||||||
|
// Calls are ordered by execution index. So the algo is quite simple.
|
||||||
|
// We loop through the flat calls, at each call, if the parent is present
|
||||||
|
// and reverted, the current call is reverted. Otherwise, if the current call
|
||||||
|
// is failed, the state is reverted. In all other cases, we simply continue
|
||||||
|
// our iteration loop.
|
||||||
|
//
|
||||||
|
// This works because we see the parent before its children, and since we
|
||||||
|
// trickle down the state reverted value down the children, checking the parent
|
||||||
|
// of a call will always tell us if the whole chain of parent/child should
|
||||||
|
// be reverted
|
||||||
|
//
|
||||||
|
calls := f.transaction.Calls
|
||||||
|
for _, call := range f.transaction.Calls {
|
||||||
|
var parent *pbeth.Call
|
||||||
|
if call.ParentIndex > 0 {
|
||||||
|
parent = calls[call.ParentIndex-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
call.StateReverted = (parent != nil && parent.StateReverted) || call.StatusFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Firehose) removeLogBlockIndexOnStateRevertedCalls() {
|
||||||
|
for _, call := range f.transaction.Calls {
|
||||||
|
if call.StateReverted {
|
||||||
|
for _, log := range call.Logs {
|
||||||
|
log.BlockIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CaptureStart implements the EVMLogger interface to initialize the tracing operation.
|
// CaptureStart implements the EVMLogger interface to initialize the tracing operation.
|
||||||
func (f *Firehose) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
func (f *Firehose) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
||||||
f.callStart("root", rootCallType(create), from, to, input, gas, value)
|
f.callStart("root", rootCallType(create), from, to, input, gas, value)
|
||||||
|
|
@ -234,18 +281,28 @@ func (f *Firehose) CaptureEnd(output []byte, gasUsed uint64, err error) {
|
||||||
|
|
||||||
// CaptureState implements the EVMLogger interface to trace a single step of VM execution.
|
// CaptureState implements the EVMLogger interface to trace a single step of VM execution.
|
||||||
func (f *Firehose) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
func (f *Firehose) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
||||||
if activeCall := f.callStack.Peek(); activeCall != nil && activeCall.CallType != pbeth.CallType_CREATE {
|
f.captureInterpreterStep(pc, op, gas, cost, scope, rData, depth, err)
|
||||||
activeCall.ExecutedCode = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaptureFault implements the EVMLogger interface to trace an execution fault.
|
// CaptureFault implements the EVMLogger interface to trace an execution fault.
|
||||||
func (f *Firehose) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) {
|
func (f *Firehose) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
|
||||||
if activeCall := f.callStack.Peek(); activeCall != nil && activeCall.CallType != pbeth.CallType_CREATE {
|
f.captureInterpreterStep(pc, op, gas, cost, scope, nil, depth, err)
|
||||||
activeCall.ExecutedCode = true
|
}
|
||||||
|
|
||||||
|
func (f *Firehose) captureInterpreterStep(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, rData []byte, depth int, err error) {
|
||||||
|
activeCall := f.callStack.Peek()
|
||||||
|
if activeCall == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Maybe we will need this to properly track status failed/reverted of
|
// Firehose model don't today flag contract creation as having executed code. But it we
|
||||||
|
// keep like that for backward compatibility.
|
||||||
|
//
|
||||||
|
// However, if at some point we bump to fixes the small bug here and there, we should
|
||||||
|
// actually flag contract creation as having executed code only if a constructor was called.
|
||||||
|
if activeCall.CallType != pbeth.CallType_CREATE {
|
||||||
|
activeCall.ExecutedCode = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firehose) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
func (f *Firehose) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||||
|
|
@ -276,16 +333,24 @@ func (f *Firehose) CaptureExit(output []byte, gasUsed uint64, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firehose) callStart(source string, callType pbeth.CallType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
func (f *Firehose) callStart(source string, callType pbeth.CallType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||||
firehoseDebug("call start source=%s type=%s", source, callType)
|
firehoseDebug("call start source=%s index=%d type=%s input=%s", source, f.callStack.NextIndex(), callType, inputView(input))
|
||||||
f.ensureInBlockAndInTrx()
|
f.ensureInBlockAndInTrx()
|
||||||
|
|
||||||
|
// First to avoid paying the `bytes.Clone` below
|
||||||
|
if callType == pbeth.CallType_CREATE {
|
||||||
|
// Replicates a previous bug in Firehose instrumentation where create calls input is always
|
||||||
|
// set to `nil` (this was done on purpose but it missed the fact that we are missing the
|
||||||
|
// actual input data of the constuctor call, if present and invoked).
|
||||||
|
input = nil
|
||||||
|
}
|
||||||
|
|
||||||
call := &pbeth.Call{
|
call := &pbeth.Call{
|
||||||
BeginOrdinal: f.blockOrdinal.Next(),
|
BeginOrdinal: f.blockOrdinal.Next(),
|
||||||
CallType: callType,
|
CallType: callType,
|
||||||
Depth: 0,
|
Depth: 0,
|
||||||
Caller: from.Bytes(),
|
Caller: from.Bytes(),
|
||||||
Address: to.Bytes(),
|
Address: to.Bytes(),
|
||||||
Input: input,
|
Input: bytes.Clone(input),
|
||||||
Value: firehoseBigIntFromNative(value),
|
Value: firehoseBigIntFromNative(value),
|
||||||
GasLimit: gas,
|
GasLimit: gas,
|
||||||
}
|
}
|
||||||
|
|
@ -299,18 +364,11 @@ func (f *Firehose) callStart(source string, callType pbeth.CallType, from common
|
||||||
call.BeginOrdinal = 0
|
call.BeginOrdinal = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if callType == pbeth.CallType_CREATE {
|
|
||||||
// Replicates a previous bug in Firehose instrumentation where create calls input is always
|
|
||||||
// set to `nil` (this was done on purpose but it missed the fact that we are missing the
|
|
||||||
// actual input data of the constuctor call, if present and invoked).
|
|
||||||
call.Input = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f.callStack.Push(call)
|
f.callStack.Push(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firehose) callEnd(source string, output []byte, gasUsed uint64, err error) {
|
func (f *Firehose) callEnd(source string, output []byte, gasUsed uint64, err error) {
|
||||||
firehoseDebug("call end source=%s output=%s gasUsed=%d err=%s", source, outputView(output), gasUsed, errorView(err))
|
firehoseDebug("call end source=%s index=%d output=%s gasUsed=%d err=%s", source, f.callStack.ActiveIndex(), outputView(output), gasUsed, errorView(err))
|
||||||
|
|
||||||
if f.latestCallStartSuicided {
|
if f.latestCallStartSuicided {
|
||||||
if source != "child" {
|
if source != "child" {
|
||||||
|
|
@ -330,16 +388,21 @@ func (f *Firehose) callEnd(source string, output []byte, gasUsed uint64, err err
|
||||||
|
|
||||||
// For create call, we do not save the returned value which is the actual contract's code
|
// For create call, we do not save the returned value which is the actual contract's code
|
||||||
if call.CallType != pbeth.CallType_CREATE {
|
if call.CallType != pbeth.CallType_CREATE {
|
||||||
call.ReturnData = output
|
call.ReturnData = bytes.Clone(output)
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Unset field for now, need to ensure they are tracked somewere
|
// Pre-compiled addresses always execute code
|
||||||
// call.StateReverted
|
if f.isPrecompiledAddr(common.BytesToAddress(call.Address)) {
|
||||||
|
call.ExecutedCode = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
call.FailureReason = err.Error()
|
call.FailureReason = err.Error()
|
||||||
call.StatusFailed = true
|
call.StatusFailed = true
|
||||||
call.StatusReverted = errors.Is(err, vm.ErrExecutionReverted)
|
|
||||||
|
// We also treat ErrInsufficientBalance and ErrDepth as reverted in Firehose model
|
||||||
|
// because they do not cost any gas.
|
||||||
|
call.StatusReverted = errors.Is(err, vm.ErrExecutionReverted) || errors.Is(err, vm.ErrInsufficientBalance) || errors.Is(err, vm.ErrDepth)
|
||||||
}
|
}
|
||||||
|
|
||||||
call.EndOrdinal = f.blockOrdinal.Next()
|
call.EndOrdinal = f.blockOrdinal.Next()
|
||||||
|
|
@ -605,10 +668,12 @@ func (f *Firehose) OnGasConsumed(gas, amount uint64) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
firehoseTrace("gas consumed before=%d after=%d", gas, gas-amount)
|
||||||
|
|
||||||
activeCall := f.callStack.Peek()
|
activeCall := f.callStack.Peek()
|
||||||
change := &pbeth.GasChange{
|
change := &pbeth.GasChange{
|
||||||
OldValue: gas,
|
OldValue: gas,
|
||||||
NewValue: gas + amount,
|
NewValue: gas - amount,
|
||||||
Ordinal: f.blockOrdinal.Next(),
|
Ordinal: f.blockOrdinal.Next(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -979,6 +1044,12 @@ func firehoseDebug(msg string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func firehoseTrace(msg string, args ...interface{}) {
|
||||||
|
if isFirehoseTracerEnabled {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Firehose] "+msg+"\n", args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore unused, we keep it around for debugging purposes
|
// Ignore unused, we keep it around for debugging purposes
|
||||||
var _ = firehoseDebugPrintStack
|
var _ = firehoseDebugPrintStack
|
||||||
|
|
||||||
|
|
@ -1018,6 +1089,10 @@ type CallStack struct {
|
||||||
depth int
|
depth int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewCallStack() *CallStack {
|
||||||
|
return &CallStack{}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *CallStack) Reset() {
|
func (s *CallStack) Reset() {
|
||||||
s.index = 0
|
s.index = 0
|
||||||
s.stack = s.stack[:0]
|
s.stack = s.stack[:0]
|
||||||
|
|
@ -1046,6 +1121,18 @@ func (s *CallStack) Push(call *pbeth.Call) {
|
||||||
s.stack = append(s.stack, call)
|
s.stack = append(s.stack, call)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *CallStack) ActiveIndex() uint32 {
|
||||||
|
if len(s.stack) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.stack[len(s.stack)-1].Index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CallStack) NextIndex() uint32 {
|
||||||
|
return s.index + 1
|
||||||
|
}
|
||||||
|
|
||||||
func (s *CallStack) Pop() (out *pbeth.Call) {
|
func (s *CallStack) Pop() (out *pbeth.Call) {
|
||||||
if len(s.stack) == 0 {
|
if len(s.stack) == 0 {
|
||||||
panic(fmt.Errorf("pop from empty call stack"))
|
panic(fmt.Errorf("pop from empty call stack"))
|
||||||
|
|
@ -1077,6 +1164,10 @@ type DeferredCallState struct {
|
||||||
nonceChanges []*pbeth.NonceChange
|
nonceChanges []*pbeth.NonceChange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewDeferredCallState() *DeferredCallState {
|
||||||
|
return &DeferredCallState{}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DeferredCallState) MaybePopulateCallAndReset(source string, call *pbeth.Call) error {
|
func (d *DeferredCallState) MaybePopulateCallAndReset(source string, call *pbeth.Call) error {
|
||||||
if d.IsEmpty() {
|
if d.IsEmpty() {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func TestFirehoseCallStack_Push(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := &CallStack{}
|
s := NewCallStack()
|
||||||
|
|
||||||
for _, action := range tt.actions {
|
for _, action := range tt.actions {
|
||||||
action(t, s)
|
action(t, s)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue