Skip to content

Commit ba6af44

Browse files
authored
feat: trace single preconfirmed transaction (#3002)
trace-single
1 parent 0aa9bbe commit ba6af44

File tree

10 files changed

+317
-28
lines changed

10 files changed

+317
-28
lines changed

core/pending.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type PendingData interface {
1818
GetStateUpdate() *StateUpdate
1919
GetNewClasses() map[felt.Felt]Class
2020
GetCandidateTransaction() []Transaction
21+
GetTransactionStateDiffs() []*StateDiff
2122
Variant() PendingDataVariant
2223
}
2324

@@ -64,6 +65,10 @@ func (p *PreConfirmed) GetCandidateTransaction() []Transaction {
6465
return p.CandidateTxs
6566
}
6667

68+
func (p *PreConfirmed) GetTransactionStateDiffs() []*StateDiff {
69+
return p.TransactionStateDiffs
70+
}
71+
6772
func (p *PreConfirmed) Variant() PendingDataVariant {
6873
return PreConfirmedBlockVariant
6974
}

mocks/mock_synchronizer.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rpc/v8/subscriptions_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,9 @@ func (fs *fakeSyncer) PendingData() (core.PendingData, error) {
400400
}
401401
func (fs *fakeSyncer) PendingBlock() *core.Block { return nil }
402402
func (fs *fakeSyncer) PendingState() (core.StateReader, func() error, error) { return nil, nil, nil }
403+
func (fs *fakeSyncer) PendingStateBeforeIndex(index int) (core.StateReader, func() error, error) {
404+
return nil, nil, nil
405+
}
403406

404407
func TestSubscribeNewHeads(t *testing.T) {
405408
log := utils.NewNopZapLogger()

rpc/v9/subscriptions_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,9 @@ func (fs *fakeSyncer) PendingData() (core.PendingData, error) {
452452
}
453453
func (fs *fakeSyncer) PendingBlock() *core.Block { return nil }
454454
func (fs *fakeSyncer) PendingState() (core.StateReader, func() error, error) { return nil, nil, nil }
455+
func (fs *fakeSyncer) PendingStateBeforeIndex(index int) (core.StateReader, func() error, error) {
456+
return nil, nil, nil
457+
}
455458

456459
func TestSubscribeNewHeads(t *testing.T) {
457460
log := utils.NewNopZapLogger()

rpc/v9/trace.go

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7+
"fmt"
78
"net/http"
89
"slices"
910
"strconv"
@@ -72,46 +73,130 @@ type FunctionInvocation struct {
7273
//
7374
// It follows the specification defined here:
7475
// https://github.com/starkware-libs/starknet-specs/blob/9377851884da5c81f757b6ae0ed47e84f9e7c058/api/starknet_trace_api_openrpc.json#L11
75-
func (h *Handler) TraceTransaction(ctx context.Context, hash felt.Felt) (*TransactionTrace, http.Header, *jsonrpc.Error) {
76+
func (h *Handler) TraceTransaction(ctx context.Context, hash felt.Felt) (TransactionTrace, http.Header, *jsonrpc.Error) {
7677
_, blockHash, _, err := h.bcReader.Receipt(&hash)
7778
httpHeader := http.Header{}
7879
httpHeader.Set(ExecutionStepsHeader, "0")
7980

8081
if err != nil && !errors.Is(err, db.ErrKeyNotFound) {
81-
return nil, httpHeader, rpccore.ErrTxnHashNotFound
82+
return TransactionTrace{}, httpHeader, rpccore.ErrTxnHashNotFound
8283
}
8384

84-
var block *core.Block
85-
isPendingBlock := blockHash == nil
86-
if isPendingBlock {
87-
var pending core.PendingData
88-
pending, err = h.PendingData()
85+
isPreConfirmed := blockHash == nil
86+
if !isPreConfirmed {
87+
block, err := h.bcReader.BlockByHash(blockHash)
8988
if err != nil {
9089
// for traceTransaction handlers there is no block not found error
91-
return nil, httpHeader, rpccore.ErrTxnHashNotFound
90+
return TransactionTrace{}, httpHeader, rpccore.ErrTxnHashNotFound
9291
}
93-
block = pending.GetBlock()
94-
} else {
95-
block, err = h.bcReader.BlockByHash(blockHash)
96-
if err != nil {
97-
// for traceTransaction handlers there is no block not found error
98-
return nil, httpHeader, rpccore.ErrTxnHashNotFound
92+
93+
txIndex := slices.IndexFunc(block.Transactions, func(tx core.Transaction) bool {
94+
return tx.Hash().Equal(&hash)
95+
})
96+
97+
if txIndex == -1 {
98+
return TransactionTrace{}, httpHeader, rpccore.ErrTxnHashNotFound
99+
}
100+
101+
blockTraces, httphttpHeader, rpcErr := h.traceBlockTransactions(ctx, block)
102+
if rpcErr != nil {
103+
return TransactionTrace{}, httphttpHeader, rpcErr
99104
}
105+
return *blockTraces[txIndex].TraceRoot, httphttpHeader, nil
106+
}
107+
108+
var pendingData core.PendingData
109+
pendingData, err = h.PendingData()
110+
if err != nil {
111+
// for traceTransaction handlers there is no block not found error
112+
return TransactionTrace{}, httpHeader, rpccore.ErrTxnHashNotFound
100113
}
114+
block := pendingData.GetBlock()
101115

102116
txIndex := slices.IndexFunc(block.Transactions, func(tx core.Transaction) bool {
103117
return tx.Hash().Equal(&hash)
104118
})
105119
if txIndex == -1 {
106-
return nil, httpHeader, rpccore.ErrTxnHashNotFound
120+
return TransactionTrace{}, httpHeader, rpccore.ErrTxnHashNotFound
121+
}
122+
123+
switch v := pendingData.Variant(); v {
124+
case core.PendingBlockVariant:
125+
blockTraces, httphttpHeader, rpcErr := h.traceBlockTransactions(ctx, block)
126+
if rpcErr != nil {
127+
return TransactionTrace{}, httphttpHeader, rpcErr
128+
}
129+
return *blockTraces[txIndex].TraceRoot, httphttpHeader, nil
130+
case core.PreConfirmedBlockVariant:
131+
return h.tracePreConfirmedTransaction(block, txIndex)
132+
default:
133+
panic(fmt.Errorf("unknown pending data variant: %v", v))
134+
}
135+
}
136+
137+
func (h *Handler) tracePreConfirmedTransaction(block *core.Block, txIndex int) (TransactionTrace, http.Header, *jsonrpc.Error) {
138+
httpHeader := http.Header{}
139+
httpHeader.Set(ExecutionStepsHeader, "0")
140+
state, stateCloser, err := h.syncReader.PendingStateBeforeIndex(txIndex)
141+
if err != nil {
142+
return TransactionTrace{}, httpHeader, jsonrpc.Err(jsonrpc.InternalError, err.Error())
107143
}
144+
defer h.callAndLogErr(stateCloser, "Failed to close head state in TraceTransaction")
145+
146+
var classes []core.Class
147+
paidFeesOnL1 := []*felt.Felt{}
148+
149+
transaction := block.Transactions[txIndex]
150+
switch tx := transaction.(type) {
151+
/// TODO(Ege): decide what to do with this, should be an edge case
152+
case *core.DeclareTransaction:
153+
class, stateErr := state.Class(tx.ClassHash)
154+
if stateErr != nil {
155+
return TransactionTrace{}, httpHeader, jsonrpc.Err(jsonrpc.InternalError, stateErr.Error())
156+
}
157+
classes = append(classes, class.Class)
158+
case *core.L1HandlerTransaction:
159+
var fee felt.Felt
160+
paidFeesOnL1 = append(paidFeesOnL1, fee.SetUint64(1))
161+
}
162+
163+
blockHashToBeRevealed, err := h.getRevealedBlockHash(block.Number)
164+
if err != nil {
165+
return TransactionTrace{}, httpHeader, rpccore.ErrInternal.CloneWithData(err)
166+
}
167+
network := h.bcReader.Network()
168+
header := block.Header
169+
blockInfo := vm.BlockInfo{
170+
Header: header,
171+
BlockHashToBeRevealed: blockHashToBeRevealed,
172+
}
173+
174+
executionResult, err := h.vm.Execute([]core.Transaction{transaction}, classes, paidFeesOnL1,
175+
&blockInfo, state, network, false, false, false, true, false)
176+
177+
httpHeader.Set(ExecutionStepsHeader, strconv.FormatUint(executionResult.NumSteps, 10))
178+
179+
if err != nil {
180+
if errors.Is(err, utils.ErrResourceBusy) {
181+
return TransactionTrace{}, httpHeader, rpccore.ErrInternal.CloneWithData(rpccore.ThrottledVMErr)
182+
}
183+
// Since we are tracing an existing block, we know that there should be no errors during execution. If we encounter any,
184+
// report them as unexpected errors
185+
return TransactionTrace{}, httpHeader, rpccore.ErrUnexpectedError.CloneWithData(err.Error())
186+
}
187+
188+
// Adapt vm transaction trace to rpc v9 trace and add root level execution resources
189+
trace := AdaptVMTransactionTrace(&executionResult.Traces[0])
108190

109-
traceResults, header, traceBlockErr := h.traceBlockTransactions(ctx, block)
110-
if traceBlockErr != nil {
111-
return nil, header, traceBlockErr
191+
trace.ExecutionResources = &ExecutionResources{
192+
InnerExecutionResources: InnerExecutionResources{
193+
L1Gas: executionResult.GasConsumed[0].L1Gas,
194+
L2Gas: executionResult.GasConsumed[0].L2Gas,
195+
},
196+
L1DataGas: executionResult.GasConsumed[0].L1DataGas,
112197
}
113198

114-
return traceResults[txIndex].TraceRoot, header, nil
199+
return trace, httpHeader, nil
115200
}
116201

117202
// TraceBlockTransactions returns the trace for a given blockID

rpc/v9/trace_test.go

Lines changed: 98 additions & 8 deletions
Large diffs are not rendered by default.

sequencer/sequencer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ func (s *Sequencer) PendingState() (core.StateReader, func() error, error) {
214214
return s.builder.PendingState(s.buildState)
215215
}
216216

217+
func (s *Sequencer) PendingStateBeforeIndex(index int) (core.StateReader, func() error, error) {
218+
return nil, nil, errors.ErrUnsupported
219+
}
220+
217221
func (s *Sequencer) HighestBlockHeader() *core.Header {
218222
return nil // Not relevant for Sequencer. Todo: clean Reader
219223
}

sync/pending.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ func (p *Pending) GetCandidateTransaction() []core.Transaction {
5050
return []core.Transaction{}
5151
}
5252

53+
func (p *Pending) GetTransactionStateDiffs() []*core.StateDiff {
54+
return []*core.StateDiff{}
55+
}
56+
5357
func (p *Pending) Variant() core.PendingDataVariant {
5458
return core.PendingBlockVariant
5559
}

sync/sync.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ type Reader interface {
7777
PendingData() (core.PendingData, error)
7878
PendingBlock() *core.Block
7979
PendingState() (core.StateReader, func() error, error)
80+
PendingStateBeforeIndex(index int) (core.StateReader, func() error, error)
8081
}
8182

8283
// This is temporary and will be removed once the p2p synchronizer implements this interface.
@@ -114,6 +115,10 @@ func (n *NoopSynchronizer) PendingState() (core.StateReader, func() error, error
114115
return nil, nil, errors.New("PendingState() not implemented")
115116
}
116117

118+
func (n *NoopSynchronizer) PendingStateBeforeIndex(index int) (core.StateReader, func() error, error) {
119+
return nil, nil, errors.New("PendingStateBeforeIndex() not implemented")
120+
}
121+
117122
// Synchronizer manages a list of StarknetData to fetch the latest blockchain updates
118123
type Synchronizer struct {
119124
blockchain *blockchain.Blockchain
@@ -703,7 +708,6 @@ func (s *Synchronizer) PendingData() (core.PendingData, error) {
703708
p := *ptr
704709
switch p.Variant() {
705710
case core.PreConfirmedBlockVariant:
706-
// pre_confirmed
707711
expectedOldRoot := &felt.Zero
708712
expectedBlockNumber := uint64(0)
709713
if head, err := s.blockchain.HeadsHeader(); err == nil {
@@ -751,6 +755,30 @@ func (s *Synchronizer) PendingState() (core.StateReader, func() error, error) {
751755
return NewPendingState(pending.GetStateUpdate().StateDiff, pending.GetNewClasses(), core.NewState(txn)), noop, nil
752756
}
753757

758+
// PendingStateAfterIndex returns the state obtained by applying all transaction state diffs
759+
// up to given index in the pre-confirmed block.
760+
func (s *Synchronizer) PendingStateBeforeIndex(index int) (core.StateReader, func() error, error) {
761+
txn := s.db.NewIndexedBatch()
762+
763+
pending, err := s.PendingData()
764+
if err != nil {
765+
return nil, nil, err
766+
}
767+
768+
if pending.Variant() != core.PreConfirmedBlockVariant {
769+
return nil, nil, errors.New("only supported for pre_confirmed block")
770+
}
771+
772+
stateDiff := core.EmptyStateDiff()
773+
// Transaction state diffs size must always match Transactions
774+
txStateDiffs := pending.GetTransactionStateDiffs()
775+
for _, txStateDiff := range txStateDiffs[:index] {
776+
stateDiff.Merge(txStateDiff)
777+
}
778+
779+
return NewPendingState(&stateDiff, pending.GetNewClasses(), core.NewState(txn)), noop, nil
780+
}
781+
754782
func (s *Synchronizer) storeEmptyPendingData(lastHeader *core.Header) {
755783
blockVer, err := core.ParseBlockVersion(lastHeader.ProtocolVersion)
756784
if err == nil {

sync/sync_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,57 @@ func TestPendingData(t *testing.T) {
340340
require.NoError(t, pendingStateCloser())
341341
})
342342
})
343+
344+
t.Run("get pending state before index", func(t *testing.T) {
345+
var synchronizer *sync.Synchronizer
346+
testDB := memory.New()
347+
chain := blockchain.New(testDB, &utils.Mainnet)
348+
dataSource := sync.NewFeederGatewayDataSource(chain, gw)
349+
synchronizer = sync.New(chain, dataSource, utils.NewNopZapLogger(), 0, 0, false, testDB)
350+
351+
require.NoError(t, err)
352+
client := feeder.NewTestClient(t, &utils.SepoliaIntegration)
353+
gw := adaptfeeder.New(client)
354+
preConfirmed, err := gw.PreConfirmedBlockByNumber(t.Context(), 1204672)
355+
preConfirmed.StateUpdate.OldRoot = &felt.Zero
356+
preConfirmed.Block.Number = 0
357+
require.NoError(t, err)
358+
require.NoError(t, synchronizer.StorePreConfirmed(&preConfirmed))
359+
txCount := len(preConfirmed.GetTransactions())
360+
361+
pendingState, pendingStateCloser, pErr := synchronizer.PendingStateBeforeIndex(txCount - 1)
362+
require.NoError(t, pErr)
363+
364+
// Check storage value in two different index
365+
// See clients/feeder/testdata/sepolia-integration/pre_confirmed/1204672.json
366+
contractAddress, err := new(felt.Felt).SetString("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d")
367+
require.NoError(t, err)
368+
key, err := new(felt.Felt).SetString("0x5496768776e3db30053404f18067d81a6e06f5a2b0de326e21298fd9d569a9a")
369+
require.NoError(t, err)
370+
val, err := pendingState.ContractStorage(contractAddress, key)
371+
require.NoError(t, err)
372+
expectedVal, err := new(felt.Felt).SetString("0x1d057bfbd3cadebffd74")
373+
require.NoError(t, err)
374+
require.Equal(t, expectedVal, val)
375+
t.Cleanup(func() {
376+
require.NoError(t, pendingStateCloser())
377+
})
378+
379+
pendingState, pendingStateCloser, pErr = synchronizer.PendingStateBeforeIndex(txCount)
380+
require.NoError(t, pErr)
381+
contractAddress, err = new(felt.Felt).SetString("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d")
382+
require.NoError(t, err)
383+
key, err = new(felt.Felt).SetString("0x5496768776e3db30053404f18067d81a6e06f5a2b0de326e21298fd9d569a9a")
384+
require.NoError(t, err)
385+
val, err = pendingState.ContractStorage(contractAddress, key)
386+
require.NoError(t, err)
387+
expectedVal, err = new(felt.Felt).SetString("0x1d057bfbd3df63f5dd54")
388+
require.NoError(t, err)
389+
require.Equal(t, expectedVal, val)
390+
t.Cleanup(func() {
391+
require.NoError(t, pendingStateCloser())
392+
})
393+
})
343394
})
344395
}
345396

0 commit comments

Comments
 (0)