Skip to content

test(ethexe-runtime-common): PayloadLookup::force_stored violates 'Zero payload stored directly' invariant (auto-tester) 6938735d5395#5526

Draft
grishasobol wants to merge 1 commit into
masterfrom
auto-tester/ethexe-runtime-common-6938735d5395
Draft

test(ethexe-runtime-common): PayloadLookup::force_stored violates 'Zero payload stored directly' invariant (auto-tester) 6938735d5395#5526
grishasobol wants to merge 1 commit into
masterfrom
auto-tester/ethexe-runtime-common-6938735d5395

Conversation

@grishasobol
Copy link
Copy Markdown
Member

PayloadLookup::force_stored() called on a Direct(Payload::new()) (the canonical empty payload) writes the empty payload to storage and converts the variant to Stored(hash). This contradicts the documented type-level invariant on PayloadLookup at ethexe/runtime/common/src/state.rs:62:

/// Zero payload should always be stored directly.

Scenario: zero_value
Hash: 6938735d5395
Unit: ethexe-runtime-common

Observed

After PayloadLookup::empty().force_stored(&storage) the variant becomes Stored(_) while PayloadLookup::empty() initially returned Direct(Payload::new()). Reproduces 3/3 deterministically.

Rubric items satisfied

  • (a) Contradicts documented invariant. Doc comment at ethexe/runtime/common/src/state.rs:62 states verbatim: /// Zero payload should always be stored directly. Calling the public force_stored method on an empty payload moves the value from Direct to Stored, violating the invariant.

Root cause

// state.rs:98-110
pub fn force_stored<S: Storage>(&mut self, storage: &S) -> HashOf<Payload> {
    let hash = match self {
        Self::Direct(payload) => {
            let payload = mem::replace(payload, Payload::new());
            storage.write_payload(payload)   // even if payload.is_empty()
        }
        Self::Stored(hash) => *hash,
    };
    *self = hash.into();   // unconditionally → Stored
    hash
}

The method does not check payload.is_empty() before writing. The type's own invariant requires empty payloads to remain in the Direct variant; the method has no early-return for that case.

Suggested fix

Add an early return for empty payloads (no storage write, no variant change):

pub fn force_stored<S: Storage>(&mut self, storage: &S) -> HashOf<Payload> {
    if let Self::Direct(p) = self {
        if p.is_empty() {
            // Invariant: zero payload stays Direct. Return a sentinel
            // (or change the signature to Option<HashOf<Payload>>).
        }
    }
    // ... existing code
}

Or weaken the doc comment to "Zero payload is stored directly by default, unless explicitly forced via force_stored." — then update callers to never invoke it on empty payloads.

Practical reachability

Any caller that uses force_stored on a payload it didn't pre-validate as non-empty will silently violate the invariant. The variant is observable via PayloadLookup::is_empty() (returns false for the broken Stored empty) and via SCALE codec — downstream code that pattern-matches on Direct/Stored may take a different branch than expected.

Test source

// scenario: zero_value
// param_signature: PayloadLookup::force_stored on empty payload should remain Direct
// hash: 6938735d5395

use ethexe_runtime_common::state::{MemStorage, PayloadLookup};

/// Invariant from state.rs:62 -- "Zero payload should always be stored directly."
/// force_stored on an empty payload must write the payload to storage, but
/// the invariant says empty payload must stay Direct.  We probe whether
/// calling force_stored on an empty Direct payload produces a Stored variant
/// (which would violate the documented invariant) or a Direct empty variant.
#[test]
fn zero_payload_force_stored_violates_invariant() {
    let storage = MemStorage::default();

    let mut lookup = PayloadLookup::empty();
    assert!(lookup.is_empty(), "should start empty");

    // After force_stored the value is always converted to Stored(hash).
    // But the doc says "Zero payload should always be stored directly."
    // So a Stored result for an empty payload is a violation.
    let _hash = lookup.force_stored(&storage);

    // If the invariant holds, `lookup` should still be Direct(empty).
    // If it became Stored, the invariant is broken.
    assert!(
        lookup.is_empty(),
        "Invariant violation: empty payload became Stored after force_stored; \
         'Zero payload should always be stored directly.' (state.rs:62)"
    );
}

Generated by /gear-dev:tester (auto-tester). This is a draft PR — requires human review before merge.

…ro payload stored directly' invariant (auto-tester) 6938735d5395

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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