Skip to content

Commit

Permalink
Added re-ordering of transactions ordinals when needed if parallel tr…
Browse files Browse the repository at this point in the history
…ansaction were recorded
  • Loading branch information
maoueh committed Apr 10, 2024
1 parent 37f24d4 commit 7e9ed15
Show file tree
Hide file tree
Showing 3 changed files with 421 additions and 8 deletions.
124 changes: 116 additions & 8 deletions x/evm/tracers/firehose.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func newSeiFirehoseTracer(tracerURL *url.URL) (*seitracing.Hooks, error) {
OnSeiBlockEnd: tracer.OnBlockEnd,

GetTxTracer: func(txIndex int) sdk.TxTracer {
tracer.blockReorderOrdinalOnce.Do(func() {
tracer.blockReorderOrdinal = true
tracer.blockReorderOrdinalSnapshot = tracer.blockOrdinal.value
})

// Created first so we can get the pointer id everywhere
isolatedTracer := &TxTracerHooks{}
isolatedTracerID := fmt.Sprintf("%03d-%p", txIndex, isolatedTracer)
Expand Down Expand Up @@ -182,12 +187,14 @@ type Firehose struct {
applyBackwardCompatibility *bool

// Block state
block *pbeth.Block
blockBaseFee *big.Int
blockOrdinal *Ordinal
blockFinality *FinalityStatus
blockRules params.Rules
blockReorderOrdinals bool
block *pbeth.Block
blockBaseFee *big.Int
blockOrdinal *Ordinal
blockFinality *FinalityStatus
blockRules params.Rules
blockReorderOrdinal bool
blockReorderOrdinalSnapshot uint64
blockReorderOrdinalOnce sync.Once

// Transaction state
evm *tracing.VMContext
Expand Down Expand Up @@ -217,8 +224,9 @@ func NewFirehose(config *FirehoseConfig) *Firehose {
applyBackwardCompatibility: config.ApplyBackwardCompatibility,

// Block state
blockOrdinal: &Ordinal{},
blockFinality: &FinalityStatus{},
blockOrdinal: &Ordinal{},
blockFinality: &FinalityStatus{},
blockReorderOrdinal: false,

// Transaction state
transactionLogIndex: 0,
Expand Down Expand Up @@ -268,6 +276,9 @@ func (f *Firehose) resetBlock() {
f.blockFinality.Reset()
f.blockIsPrecompiledAddr = nil
f.blockRules = params.Rules{}
f.blockReorderOrdinal = false
f.blockReorderOrdinalSnapshot = 0
f.blockReorderOrdinalOnce = sync.Once{}
}

// resetTransaction resets the transaction state and the call state in one shot
Expand Down Expand Up @@ -398,6 +409,10 @@ func (f *Firehose) OnBlockEnd(err error) {
firehoseInfo("block ending (err=%s)", errorView(err))

if err == nil {
if f.blockReorderOrdinal {
f.reorderIsolatedTransactionsAndOrdinals()
}

f.ensureInBlockAndNotInTrx()
f.printBlockToFirehose(f.block, f.blockFinality)
} else {
Expand All @@ -411,6 +426,93 @@ func (f *Firehose) OnBlockEnd(err error) {
firehoseInfo("block end")
}

// reorderIsolatedTransactionsAndOrdinals is called right after all transactions have completed execution. It will sort transactions
// according to their index.
//
// But most importantly, will re-assign all the ordinals of each transaction recursively. When the parallel execution happened,
// all ordinal were made relative to the transaction they were contained in. But now, we are going to re-assign them to the
// global block ordinal by getting the current ordinal and ad it to the transaction ordinal and so forth.
func (f *Firehose) reorderIsolatedTransactionsAndOrdinals() {
if !f.blockReorderOrdinal {
firehoseInfo("post process isolated transactions skipped (block_reorder_ordinals=false)")
return
}

ordinalBase := f.blockReorderOrdinalSnapshot
firehoseInfo("post processing isolated transactions sorting & re-assigning ordinals (ordinal_base=%d)", ordinalBase)

slices.SortStableFunc(f.block.TransactionTraces, func(i, j *pbeth.TransactionTrace) int {
return int(i.Index) - int(j.Index)
})

baseline := ordinalBase
for _, trx := range f.block.TransactionTraces {
trx.BeginOrdinal += baseline
for _, call := range trx.Calls {
f.reorderCallOrdinals(call, baseline)
}

for _, log := range trx.Receipt.Logs {
log.Ordinal += baseline
}

trx.EndOrdinal += baseline
baseline = trx.EndOrdinal
}

for _, ch := range f.block.BalanceChanges {
if ch.Ordinal >= ordinalBase {
ch.Ordinal += baseline
}
}
for _, ch := range f.block.CodeChanges {
if ch.Ordinal >= ordinalBase {
ch.Ordinal += baseline
}
}
for _, call := range f.block.SystemCalls {
if call.BeginOrdinal >= ordinalBase {
f.reorderCallOrdinals(call, baseline)
}
}
}

func (f *Firehose) reorderCallOrdinals(call *pbeth.Call, ordinalBase uint64) (ordinalEnd uint64) {
if *f.applyBackwardCompatibility {
if call.BeginOrdinal != 0 {
call.BeginOrdinal += ordinalBase // consistent with a known small bug: root call has beginOrdinal set to 0
}
} else {
call.BeginOrdinal += ordinalBase
}

for _, log := range call.Logs {
log.Ordinal += ordinalBase
}
for _, act := range call.AccountCreations {
act.Ordinal += ordinalBase
}
for _, ch := range call.BalanceChanges {
ch.Ordinal += ordinalBase
}
for _, ch := range call.GasChanges {
ch.Ordinal += ordinalBase
}
for _, ch := range call.NonceChanges {
ch.Ordinal += ordinalBase
}
for _, ch := range call.StorageChanges {
ch.Ordinal += ordinalBase
}
for _, ch := range call.CodeChanges {
ch.Ordinal += ordinalBase
}

call.EndOrdinal += ordinalBase

return call.EndOrdinal
}

func (f *Firehose) OnBeaconBlockRootStart(root common.Hash) {
firehoseInfo("system call start (for=%s)", "beacon_block_root")
f.ensureInBlockAndNotInTrx()
Expand Down Expand Up @@ -1691,6 +1793,12 @@ func (o *Ordinal) Reset() {
o.value = 0
}

// Peek gives you the current ordinal value which is actually the last assigned
// value attributed, the next value that is going to be used is `Peek() + 1`.
func (o *Ordinal) Peek() (out uint64) {
return o.value
}

// Next gives you the next sequential ordinal value that you should
// use to assign to your exeuction trace (block, transaction, call, etc).
func (o *Ordinal) Next() (out uint64) {
Expand Down
183 changes: 183 additions & 0 deletions x/evm/tracers/firehose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import (
"encoding/json"
"fmt"
"math/big"
"os"
"reflect"
"slices"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
pbeth "github.com/sei-protocol/sei-chain/pb/sf/ethereum/type/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand Down Expand Up @@ -286,3 +293,179 @@ func filter[S ~[]T, T any](s S, f func(T) bool) (out S) {

return out
}

func TestFirehose_reorderIsolatedTransactionsAndOrdinals(t *testing.T) {
tests := []struct {
name string
populate func(t *Firehose)
expectedBlockFile string
}{
{
name: "empty",
populate: func(t *Firehose) {
t.OnBlockStart(blockEvent(1))

// Simulated GetTxTracer being called
t.blockReorderOrdinalOnce.Do(func() {
t.blockReorderOrdinal = true
t.blockReorderOrdinalSnapshot = t.blockOrdinal.value
})

t.blockOrdinal.Reset()
t.onTxStart(txEvent(), hex2Hash("CC"), from, to)
t.OnCallEnter(0, byte(vm.CALL), from, to, nil, 0, nil)
t.OnBalanceChange(empty, b(1), b(2), 0)
t.OnCallExit(0, nil, 0, nil, false)
t.OnTxEnd(txReceiptEvent(2), nil)

t.blockOrdinal.Reset()
t.onTxStart(txEvent(), hex2Hash("AA"), from, to)
t.OnCallEnter(0, byte(vm.CALL), from, to, nil, 0, nil)
t.OnBalanceChange(empty, b(1), b(2), 0)
t.OnCallExit(0, nil, 0, nil, false)
t.OnTxEnd(txReceiptEvent(0), nil)

t.blockOrdinal.Reset()
t.onTxStart(txEvent(), hex2Hash("BB"), from, to)
t.OnCallEnter(0, byte(vm.CALL), from, to, nil, 0, nil)
t.OnBalanceChange(empty, b(1), b(2), 0)
t.OnCallExit(0, nil, 0, nil, false)
t.OnTxEnd(txReceiptEvent(1), nil)
},
expectedBlockFile: "testdata/firehose/reorder-ordinals-empty.golden.json",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := NewFirehose(&FirehoseConfig{
ApplyBackwardCompatibility: ptr(false),
})
f.OnBlockchainInit(params.AllEthashProtocolChanges)

tt.populate(f)

f.reorderIsolatedTransactionsAndOrdinals()

goldenUpdate := os.Getenv("GOLDEN_UPDATE") == "true"
goldenPath := tt.expectedBlockFile

if !goldenUpdate && !fileExits(t, goldenPath) {
t.Fatalf("the golden file %q does not exist, re-run with 'GOLDEN_UPDATE=true go test ./... -run %q' to generate the intial version", goldenPath, t.Name())
}

content, err := protojson.MarshalOptions{Indent: " "}.Marshal(f.block)
require.NoError(t, err)

if goldenUpdate {
require.NoError(t, os.WriteFile(goldenPath, content, os.ModePerm))
}

expected, err := os.ReadFile(goldenPath)
require.NoError(t, err)

expectedBlock := &pbeth.Block{}
protojson.Unmarshal(expected, expectedBlock)

if !proto.Equal(expectedBlock, f.block) {
assert.Equal(t, expectedBlock, f.block, "Run 'GOLDEN_UPDATE=true go test ./... -run %q' to update golden file", t.Name())
}

seenOrdinals := make(map[uint64]int)

walkChanges(f.block.BalanceChanges, seenOrdinals)
walkChanges(f.block.CodeChanges, seenOrdinals)
walkCalls(f.block.SystemCalls, seenOrdinals)

for _, trx := range f.block.TransactionTraces {
seenOrdinals[trx.BeginOrdinal] = seenOrdinals[trx.BeginOrdinal] + 1
seenOrdinals[trx.EndOrdinal] = seenOrdinals[trx.EndOrdinal] + 1
walkCalls(trx.Calls, seenOrdinals)
}

// No ordinal should be seen more than once
for ordinal, count := range seenOrdinals {
assert.Equal(t, 1, count, "Ordinal %d seen %d times", ordinal, count)
}

ordinals := maps.Keys(seenOrdinals)
slices.Sort(ordinals)

// All ordinals should be in stricly increasing order
prev := -1
for _, ordinal := range ordinals {
if prev != -1 {
assert.Equal(t, prev+1, int(ordinal), "Ordinal %d is not in sequence", ordinal)
}
}
})
}
}

func walkCalls(calls []*pbeth.Call, ordinals map[uint64]int) {
for _, call := range calls {
walkCall(call, ordinals)
}
}

func walkCall(call *pbeth.Call, ordinals map[uint64]int) {
ordinals[call.BeginOrdinal] = ordinals[call.BeginOrdinal] + 1
ordinals[call.EndOrdinal] = ordinals[call.EndOrdinal] + 1

walkChanges(call.BalanceChanges, ordinals)
walkChanges(call.CodeChanges, ordinals)
walkChanges(call.Logs, ordinals)
walkChanges(call.StorageChanges, ordinals)
walkChanges(call.NonceChanges, ordinals)
walkChanges(call.GasChanges, ordinals)
}

func walkChanges[T any](changes []T, ordinals map[uint64]int) {
for _, change := range changes {
var x any = change
if v, ok := x.(interface{ GetOrdinal() uint64 }); ok {
ordinals[v.GetOrdinal()] = ordinals[v.GetOrdinal()] + 1
}
}
}

var b = big.NewInt
var empty, from, to = common.HexToAddress("00"), common.HexToAddress("01"), common.HexToAddress("02")
var emptyHash = common.Hash{}
var hex2Hash = common.HexToHash

func fileExits(t *testing.T, path string) bool {
t.Helper()
stat, err := os.Stat(path)
return err == nil && !stat.IsDir()
}

func txEvent() *types.Transaction {
return types.NewTx(&types.LegacyTx{
Nonce: 0,
GasPrice: big.NewInt(1),
Gas: 1,
To: &to,
Value: big.NewInt(1),
Data: nil,
V: big.NewInt(1),
R: big.NewInt(1),
S: big.NewInt(1),
})
}

func txReceiptEvent(txIndex uint) *types.Receipt {
return &types.Receipt{
Status: 1,
TransactionIndex: txIndex,
}
}

func blockEvent(height uint64) tracing.BlockEvent {
return tracing.BlockEvent{
Block: types.NewBlock(&types.Header{
Number: big.NewInt(int64(height)),
}, nil, nil, nil, nil),
TD: b(1),
}
}
Loading

0 comments on commit 7e9ed15

Please sign in to comment.