From 7e9ed159908b2263c7ff799e4d20480513111c25 Mon Sep 17 00:00:00 2001 From: Matthieu Vachon Date: Tue, 9 Apr 2024 18:31:37 -0400 Subject: [PATCH] Added re-ordering of transactions ordinals when needed if parallel transaction were recorded --- x/evm/tracers/firehose.go | 124 +++++++++++- x/evm/tracers/firehose_test.go | 183 ++++++++++++++++++ .../reorder-ordinals-empty.golden.json | 122 ++++++++++++ 3 files changed, 421 insertions(+), 8 deletions(-) create mode 100755 x/evm/tracers/testdata/firehose/reorder-ordinals-empty.golden.json diff --git a/x/evm/tracers/firehose.go b/x/evm/tracers/firehose.go index d683262a5..6b99d5d15 100644 --- a/x/evm/tracers/firehose.go +++ b/x/evm/tracers/firehose.go @@ -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) @@ -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 @@ -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, @@ -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 @@ -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 { @@ -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() @@ -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) { diff --git a/x/evm/tracers/firehose_test.go b/x/evm/tracers/firehose_test.go index 7c797453e..8ead2cb0c 100644 --- a/x/evm/tracers/firehose_test.go +++ b/x/evm/tracers/firehose_test.go @@ -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" ) @@ -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), + } +} diff --git a/x/evm/tracers/testdata/firehose/reorder-ordinals-empty.golden.json b/x/evm/tracers/testdata/firehose/reorder-ordinals-empty.golden.json new file mode 100755 index 000000000..b2ac4a9e5 --- /dev/null +++ b/x/evm/tracers/testdata/firehose/reorder-ordinals-empty.golden.json @@ -0,0 +1,122 @@ +{ + "hash": "sr0uqeYWyrLV1vijvIG6fe+0mu8LRG6FLMHv5UmUxK8=", + "number": "1", + "size": "501", + "header": { + "parentHash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "uncleHash": "HcxN6N7HXXqrhbVntszUGtMSRRuUinQT8KFC/UDUk0c=", + "coinbase": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "stateRoot": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transactionsRoot": "VugfFxvMVab/g0XmksD4bltI4BuZbK3AAWIvteNjtCE=", + "receiptRoot": "VugfFxvMVab/g0XmksD4bltI4BuZbK3AAWIvteNjtCE=", + "logsBloom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "difficulty": { + "bytes": "AA==" + }, + "totalDifficulty": { + "bytes": "AQ==" + }, + "number": "1", + "timestamp": "1970-01-01T00:00:00Z", + "mixHash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "hash": "sr0uqeYWyrLV1vijvIG6fe+0mu8LRG6FLMHv5UmUxK8=" + }, + "transactionTraces": [ + { + "to": "AAAAAAAAAAAAAAAAAAAAAAAAAAI=", + "gasPrice": { + "bytes": "AQ==" + }, + "gasLimit": "1", + "value": { + "bytes": "AQ==" + }, + "v": "AQ==", + "r": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "s": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKo=", + "from": "AAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "beginOrdinal": "1", + "endOrdinal": "4", + "status": "SUCCEEDED", + "receipt": { + "logsBloom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "calls": [ + { + "index": 1, + "callType": "CALL", + "caller": "AAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "address": "AAAAAAAAAAAAAAAAAAAAAAAAAAI=", + "beginOrdinal": "2", + "endOrdinal": "3" + } + ] + }, + { + "to": "AAAAAAAAAAAAAAAAAAAAAAAAAAI=", + "gasPrice": { + "bytes": "AQ==" + }, + "gasLimit": "1", + "value": { + "bytes": "AQ==" + }, + "v": "AQ==", + "r": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "s": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "index": 1, + "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALs=", + "from": "AAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "beginOrdinal": "5", + "endOrdinal": "8", + "status": "SUCCEEDED", + "receipt": { + "logsBloom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "calls": [ + { + "index": 1, + "callType": "CALL", + "caller": "AAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "address": "AAAAAAAAAAAAAAAAAAAAAAAAAAI=", + "beginOrdinal": "6", + "endOrdinal": "7" + } + ] + }, + { + "to": "AAAAAAAAAAAAAAAAAAAAAAAAAAI=", + "gasPrice": { + "bytes": "AQ==" + }, + "gasLimit": "1", + "value": { + "bytes": "AQ==" + }, + "v": "AQ==", + "r": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "s": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "index": 2, + "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMw=", + "from": "AAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "beginOrdinal": "9", + "endOrdinal": "12", + "status": "SUCCEEDED", + "receipt": { + "logsBloom": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "calls": [ + { + "index": 1, + "callType": "CALL", + "caller": "AAAAAAAAAAAAAAAAAAAAAAAAAAE=", + "address": "AAAAAAAAAAAAAAAAAAAAAAAAAAI=", + "beginOrdinal": "10", + "endOrdinal": "11" + } + ] + } + ], + "ver": 4 +} \ No newline at end of file