-
Notifications
You must be signed in to change notification settings - Fork 244
feat(rpc): add progressive trace range and checkpoint helpers #4079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4b3db46
11595a1
092a550
62964ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another style guide we follow is separating the conditions from the 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") | ||
| } | ||
| 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, | ||
| ) | ||
| } | ||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
| } | ||
| 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), | ||
| ) | ||
| } | ||
| 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 | ||
| } | ||
| var transactionErr vm.TransactionExecutionError | ||
| if !errors.As(err, &transactionErr) { | ||
| return err | ||
| } | ||
| transactionErr.Index += offset | ||
| return transactionErr | ||
| } | ||
|
|
||
| // 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 | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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