Skip to content

Feat: Unify AccessMetadata, add zero-copy decode, lock sort invariant#41

Merged
hmoog merged 2 commits intofeat/zk-convenantsfrom
fix/access-metadata
May 7, 2026
Merged

Feat: Unify AccessMetadata, add zero-copy decode, lock sort invariant#41
hmoog merged 2 commits intofeat/zk-convenantsfrom
fix/access-metadata

Conversation

@hmoog
Copy link
Copy Markdown
Collaborator

@hmoog hmoog commented May 6, 2026

Motivation

A malicious or buggy L1 transaction producer could declare duplicate resource_ids in its access metadata. The parent branch didn't dedup anywhere - neither the worker's extract_resources nor the guest's Payload::decode checked. This let two issues through:

  • Scheduler corruption. The scheduler links each declared access into the resource's dependency chain. Duplicates link the same tx into the same chain twice - risk of livelock (the second access waits on the first conceptually-same access), broken chain-linking invariants, or silent double-execution.
  • Journal bloat. The guest commits a separate InputResourceCommitment/OutputResourceCommitment per declared access, so duplicates produce multiple commitments for the same resource - wasted proof bytes and potentially divergent post-state.

Additionally, AccessMetadata was two structurally-identical types maintained in parallel - host-owned (vprogs_core_types::AccessMetadata) and guest-borrowed (vprogs_zk_abi::transaction_processor::AccessMetadata<'a>). The guest's Payload::decode allocated a Vec per parse, decoding entries one at a time.

Changes

Single shared AccessMetadata

  • Deleted the duplicate guest-side AccessMetadata<'a> view (zk/abi/src/transaction_processor/input/access_metadata.rs). Both host and guest now use vprogs_core_types::AccessMetadata.
  • Made the type repr(C) so its in-memory layout matches the wire byte layout exactly (33 bytes, no padding). WIRE_SIZE is now size_of::().
  • Added TryFromBytes, IntoBytes, Immutable, KnownLayout derives - a byte slice can be re-interpreted as &[AccessMetadata] zero-copy via <[AccessMetadata]>::try_ref_from_bytes.
  • Added matching zerocopy derives on ResourceId (FromBytes, IntoBytes, Immutable, KnownLayout) and AccessType (TryFromBytes, IntoBytes, Immutable, KnownLayout) so they can be fields in the zerocopy struct.

Wire format invariant + dedup

  • Access metadata is now strictly-ascending by resource_id on the wire. Verified inline during a single pass over the slice; rejects duplicates and unsorted input as the same condition (prev >= curr).
  • New: AccessMetadata::decode_slice(buf) -> &[Self] - zero-copy slice cast + sort verification (guest path). No Vec allocation.
  • New: AccessMetadata::decode_vec(buf) -> Vec - slice cast then .to_vec() (host path).
  • Removed the per-entry AccessMetadata::decode and AccessMetadata::encode inherent methods. Encoding now uses IntoBytes::as_bytes from the derive; decoding goes through the slice path.

Guest-side simplifications

  • Payload<'a> now holds access_metadata: &'a [AccessMetadata] instead of Vec<AccessMetadata<'a>> - no allocation in the parse hot path.
  • Resource<'a> now holds access_metadata: &'a AccessMetadata instead of a by-value AccessMetadata<'a>. Lifetime works out cleanly because items live in the wire buffer (via the slice cast), not in a Vec owned by Transaction.
  • Resource::decode takes &'a AccessMetadata; Inputs::decode passes meta (reference) instead of *meta (by-value copy).

Worker / scheduler integration

  • Inlined the extract_resources helper at its single call site in node/framework/src/worker.rs. Now: AccessMetadata::decode_vec(&mut tx.payload.as_slice()).unwrap_or_default().
  • Malformed, duplicate, or unsorted access metadata → empty list → tx scheduled with no resource dependencies; the original payload still flows to the prover so it can attest to invalidity.

Cleanup

  • Removed the unused From<&'a [u8; 32]> for &'a ResourceId impl (the unsafe pointer cast). Consumers were the deleted guest-side AccessMetadata<'a>::resource_id; no longer needed since the unified type holds ResourceId directly.
  • L1TransactionExt::for_l2_test (in the risc0 test-suite) sorts access_metadata internally before encoding, and uses IntoBytes::as_bytes for per-entry writes.

@hmoog hmoog changed the title fix access metadata Feat: Unify AccessMetadata, add zero-copy decode, lock sort invariant May 6, 2026
@hmoog hmoog marked this pull request as ready for review May 6, 2026 23:14
@hmoog hmoog merged commit ec4cc62 into feat/zk-convenants May 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant