Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rpc/trace_adaptation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func traceAdaptationRequest(t *testing.T, version int, feeder bool, calls int) f
record, err = tracecache.FromVM(txs, &result, false)
}
require.NoError(t, err)
record.Complete = true
cache := tracecache.New[felt.Felt, *tracecache.BlockTrace](1)
logger := log.NewNopZapLogger()
h8 := rpcv8.New(reader, nil, nil, logger).WithTraceCache(cache)
Expand Down
232 changes: 232 additions & 0 deletions rpc/tracecache/progressive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package tracecache

import (
"errors"
"fmt"

"github.com/NethermindEth/juno/core"
"github.com/NethermindEth/juno/core/felt"
"github.com/NethermindEth/juno/core/pending"
"github.com/NethermindEth/juno/vm"
)

// Range lets RPC handlers extend a cached BlockTrace without re-executing its
// transactions. Cached vm.StateDiff values provide the checkpoint the VM needs to continue.
// The handler owns execution, state-reader lifetime, and cache publication.
//
// Lifecycle:
// A handler uses it when [Cache.AcquireWithCondition] grants a lease for an unsatisfied request:
// 1. Plan the work with [PlanRange] for the requested transaction or block.
// 2. Prepare the starting state with [Range.ResumeState].
// 3. Execute transactions[Start:End] against that state.
// Use [OffsetExecutionError] with Start when reporting VM errors to the RPC caller.
// 4. Package the suffix with [FromVM] and pass it to [Range.Combine] to join the cached prefix.
// 5. Call [Lease.Publish] on success, or [Lease.Release] on failure.
//
// Example Workflow:
// A six-transaction block has two traces cached. A request arrives for transaction 4:
// - Work needed: transactions [2, 5).
// - Starting state: if the cached transactions changed a nonce from 6 to 7 to 8,
// the VM starts with nonce 8.
// - Result: the cache now covers transactions 0 through 4. A later request can
// extend it to include transaction 5.
//
// See [Cache] for the mutability contract for published traces.
type Range struct {
Start, End uint64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please one param per line.

Honestly, in this case makes a lot of sense and it is readable so feel free to ignore. It is mostly a nitpick about keeping the "status quo" style

prefix []TransactionTrace
total uint64
}

// PlanRange determines which block transactions need execution.
// The returned range uses an inclusive Start and exclusive End.
//
// Parameters:
// - cached: the block trace returned alongside the lease by
// [Cache.AcquireWithCondition].
// - transactions: the full ordered transaction list loaded for the block,
// including transactions already covered by cached.
// - target: the requested transaction's index and hash, assembled by the
// RPC handler. Nil requests execution through the block's end.
// - initialReads: when true, the entire block must be replayed from the beginning.
// The target must be nil or the last transaction.
// Request initial reads from the VM and pass true to [FromVM].
func PlanRange(
cached *BlockTrace,
transactions []core.Transaction,
target *TransactionTarget,
initialReads bool,
) (*Range, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can Range be returned by value?

if target != nil && (target.Hash == nil || target.Index >= uint64(len(transactions)) ||
!transactions[target.Index].Hash().Equal(target.Hash)) {
return nil, ErrTargetNotFound
}
Comment on lines +60 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another style guide we follow is separating the conditions from the if (or for) statements if they become too big.

Suggestion:

validTarget := <long cond>
if validTarget {
  // ...
}

if initialReads && target != nil && target.Index+1 != uint64(len(transactions)) {
return nil, errors.New("initial reads require a full block trace")
}
plan := &Range{End: uint64(len(transactions)), total: uint64(len(transactions))}
if target != nil {
plan.End = target.Index + 1
}
if cached != nil && !initialReads {
if cached.Source != LocalVM {
return nil, errors.New("cannot extend a feeder trace")

Check warning on line 73 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L73

Added line #L73 was not covered by tests
}
plan.prefix = cached.Traces
plan.Start = uint64(len(plan.prefix))
}
if plan.Start > plan.End {
return nil, fmt.Errorf(
"cached trace prefix [0, %d) exceeds requested range [0, %d)",
plan.Start,
plan.End,
)

Check warning on line 83 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L79-L83

Added lines #L79 - L83 were not covered by tests
}
return plan, nil
}

// ResumeState provides the VM's starting state for this range.
// Reconstructs the state after the cached prefix so that the VM can execute
// the remaining transactions without replaying that prefix.
//
// Parameters:
// - parent: the state reader for the state immediately before the block.
// - classes: the state reader used to load classes declared in the cached
// prefix, typically the chain's head state.
// - blockNumber: the number of the block being traced.
func (r *Range) ResumeState(
parent, classes core.StateReader,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one parameter per line

blockNumber uint64,
) (core.StateReader, error) {
if r.Start == 0 {
return parent, nil
}
checkpoint := checkpointFromTraces(r.prefix)
declared, err := loadCheckpointClasses(&checkpoint, classes)
if err != nil {
return nil, err
}
return pending.NewState(&checkpoint, declared, parent, blockNumber), nil
}

// Combine joins the executed suffix with the cached prefix into a new [BlockTrace].
//
// Parameters:
// - executed: the VM result packaged by [FromVM] after executing
// transactions[Start:End]. It must contain one trace per transaction,
// in block order, with a non-nil state diff in each trace.
//
// The result is marked complete only if this range reaches the block's end.
func (r *Range) Combine(executed *BlockTrace) (*BlockTrace, error) {
if executed.Source != LocalVM {
return nil, errors.New("cannot combine non-VM traces")

Check warning on line 122 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L122

Added line #L122 was not covered by tests
}
if uint64(len(executed.Traces)) != r.End-r.Start {
return nil, fmt.Errorf(
"VM returned an unexpected trace range: expected [%d, %d) (%d traces), received %d traces",
r.Start,
r.End,
r.End-r.Start,
len(executed.Traces),
)

Check warning on line 131 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L125-L131

Added lines #L125 - L131 were not covered by tests
}
for index := range executed.Traces {
if executed.Traces[index].vmTrace == nil || executed.Traces[index].vmTrace.StateDiff == nil {
return nil, fmt.Errorf("VM omitted state diff for transaction trace %d", r.Start+uint64(index))
}
}
result := *executed
result.Traces = make([]TransactionTrace, len(r.prefix)+len(executed.Traces))
copy(result.Traces, r.prefix)
copy(result.Traces[len(r.prefix):], executed.Traces)
result.Complete = r.End == r.total
return &result, nil
}

// OffsetExecutionError converts a vm.TransactionExecutionError index from
// suffix-relative to block-relative.
func OffsetExecutionError(err error, offset uint64) error {
if err == nil || offset == 0 {
return err

Check warning on line 150 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L149-L150

Added lines #L149 - L150 were not covered by tests
}
var transactionErr vm.TransactionExecutionError
if !errors.As(err, &transactionErr) {
return err

Check warning on line 154 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L152-L154

Added lines #L152 - L154 were not covered by tests
}
transactionErr.Index += offset
return transactionErr

Check warning on line 157 in rpc/tracecache/progressive.go

View check run for this annotation

Codecov / codecov/patch

rpc/tracecache/progressive.go#L156-L157

Added lines #L156 - L157 were not covered by tests
}

// loadCheckpointClasses loads definitions for classes declared in the cached
// prefix so ResumeState can make them available to the remaining transactions.
func loadCheckpointClasses(
diff *core.StateDiff,
classLookup core.StateReader,
) (map[felt.Felt]core.ClassDefinition, error) {
classes := make(
map[felt.Felt]core.ClassDefinition,
len(diff.DeclaredV0Classes)+len(diff.DeclaredV1Classes),
)
for _, hash := range diff.DeclaredV0Classes {
classes[*hash] = nil
}
for hash := range diff.DeclaredV1Classes {
classes[hash] = nil
}
for hash := range classes {
declared, err := classLookup.Class(&hash)
if err != nil {
return nil, err
}
classes[hash] = declared.Class
}
return classes, nil
}

// checkpointFromTraces rebuilds a core.StateDiff checkpoint from cached
// vm.StateDiff values, avoiding a duplicate cumulative diff in the cache.
func checkpointFromTraces(traces []TransactionTrace) core.StateDiff {
result := core.EmptyStateDiff()
for index := range traces {
mergeVMStateDiff(&result, traces[index].vmTrace.StateDiff)
}
return result
}

func mergeVMStateDiff(result *core.StateDiff, diff *vm.StateDiff) {
for storageIndex := range diff.StorageDiffs {
storage := &diff.StorageDiffs[storageIndex]
entries, found := result.StorageDiffs[storage.Address]
if !found {
entries = make(map[felt.Felt]*felt.Felt, len(storage.StorageEntries))
result.StorageDiffs[storage.Address] = entries
}
for entryIndex := range storage.StorageEntries {
entry := &storage.StorageEntries[entryIndex]
entries[entry.Key] = entry.Value.Clone()
}
}
for nonceIndex := range diff.Nonces {
nonce := &diff.Nonces[nonceIndex]
result.Nonces[nonce.ContractAddress] = nonce.Nonce.Clone()
}
for deployedIndex := range diff.DeployedContracts {
deployed := &diff.DeployedContracts[deployedIndex]
result.DeployedContracts[deployed.Address] = deployed.ClassHash.Clone()
}
for _, hash := range diff.DeprecatedDeclaredClasses {
result.DeclaredV0Classes = append(result.DeclaredV0Classes, hash.Clone())
}
for declaredIndex := range diff.DeclaredClasses {
declared := &diff.DeclaredClasses[declaredIndex]
result.DeclaredV1Classes[declared.ClassHash] = declared.CompiledClassHash.Clone()
}
for replacedIndex := range diff.ReplacedClasses {
replaced := &diff.ReplacedClasses[replacedIndex]
result.ReplacedClasses[replaced.ContractAddress] = replaced.ClassHash.Clone()
}
for migratedIndex := range diff.MigratedCompiledClasses {
migrated := &diff.MigratedCompiledClasses[migratedIndex]
result.MigratedClasses[migrated.ClassHash] = migrated.CompiledClassHash
}
}
Loading
Loading