Skip to content

feat(lazer): L7 — IHelperProgram.lazer_price + worked consumer#187

Merged
anil-rome merged 1 commit into
masterfrom
lazer-pyth-integration
May 19, 2026
Merged

feat(lazer): L7 — IHelperProgram.lazer_price + worked consumer#187
anil-rome merged 1 commit into
masterfrom
lazer-pyth-integration

Conversation

@anil-rome
Copy link
Copy Markdown
Contributor

Summary

Solidity side of the Pyth Lazer integration. Companion to rome-protocol/rome-evm-private#373 (which adds the on-chain wrapper at selector 0xa5f15a86).

Single commit, single PR (rome-solidity scope of the feature is minimal — interface + worked consumer).

Changes

File LOC Purpose
contracts/interface.sol +65 LazerFeedPrice struct + lazer_price(bytes,uint8,uint8) function inside IHelperProgram
contracts/examples/lazer_consumer.sol +80 (new) Reference consumer demonstrating per-feed monotonic replay protection + staleness

Spec

rome-specs/active/technical/2026-05-19-pyth-lazer-integration.md §5 (Solidity interface + worked consumer).

The interface

struct LazerFeedPrice {
    uint32 feed_id;
    int64  price;
    uint64 conf;
    int32  expo;
}

function lazer_price(
    bytes calldata envelope,
    uint8 ed25519_ix_idx,
    uint8 sig_idx
) external view returns (LazerFeedPrice[] memory feeds, uint64 publish_time_us);

Selector pinned 0xa5f15a86 = keccak256("lazer_price(bytes,uint8,uint8)")[..4], verified against the matching Rust const in rome-evm-private/program/src/non_evm/helper.rs:lazer_price_selector_locked.

Consumer pattern (per spec §5.2)

The wrapper does NOT enforce replay protection or staleness — those are consumer-side per spec §3.4. The worked example demonstrates:

mapping(uint32 => uint64) public lastAcceptedPublishTimeUs;

function getEthPrice(bytes calldata envelope, uint8 ix_idx, uint8 sig_idx)
    external returns (int64 price, int32 expo)
{
    (IHelperProgram.LazerFeedPrice[] memory feeds, uint64 publish_time_us)
        = HelperProgram.lazer_price(envelope, ix_idx, sig_idx);

    // 1. Staleness check (consumer-chosen MAX_STALENESS_US)
    require(now_us - publish_time_us <= MAX_STALENESS_US, "stale payload");

    // 2. Per-feed monotonicity (MANDATORY for multi-feed envelopes)
    for (uint i = 0; i < feeds.length; i++) {
        if (feeds[i].feed_id == ETH_FEED) {
            require(publish_time_us > lastAcceptedPublishTimeUs[ETH_FEED], "replay");
            lastAcceptedPublishTimeUs[ETH_FEED] = publish_time_us;
            return (feeds[i].price, feeds[i].expo);
        }
    }
    revert("ETH feed not in payload");
}

Per-feed (not envelope-level) monotonicity tracking is mandatory because a single envelope can carry N feeds at independent cadences — collapsing to one timestamp either rejects valid feeds or accepts replayed ones.

Merge order

This PR MUST merge AFTER rome-protocol/rome-evm-private#373:

  1. rome-evm-private#373 merges → master has the lazer_price selector at 0xa5f15a86
  2. Hadrian (or any test chain) rebuilds with new program — IHelperProgram.lazer_price is now callable
  3. This PR merges
  4. (optional) Contracts deploy the worked consumer for end-to-end smoke

If rome-solidity master ships ahead of rome-evm-private, consumer contracts calling lazer_price get a clear Unimplemented(method is not supported by HelperProgram 0xa5f15a86) — clear surface, no silent data corruption, but a deploy-time gotcha. The order matters.

Verification

  • npx hardhat compile → 46 files compiled clean (existing meteora factory size warning is unrelated)
  • npx hardhat test tests/oracle/PythPullParser.test.ts tests/oracle/SwitchboardParser.test.ts → 22/22 pass (no regressions on adjacent parser tests)

Test plan

  • Compile clean
  • No regressions on adjacent parser test suites
  • CI runs on push (build-and-test, slither, CodeQL, etc.)
  • Post-deploy: worked consumer at contracts/examples/lazer_consumer.sol deploys + can be called with a captured envelope

Solidity side of the Pyth Lazer integration. Adds the EVM-callable
surface for the wrapper that ships in rome-evm-private (master after
the lazer-pyth-integration PR merges).

## Changes

`contracts/interface.sol` (+65 LOC):
  - `LazerFeedPrice` struct inside IHelperProgram (matching the
    Rust ABI-encoded return-tuple layout from L3's encode_return):
        uint32 feed_id;
        int64  price;
        uint64 conf;
        int32  expo;
  - `lazer_price(bytes envelope, uint8 ed25519_ix_idx, uint8 sig_idx)
    external view returns (LazerFeedPrice[] memory feeds, uint64 publish_time_us)`
  - Selector pinned: `0xa5f15a86` (verified against `cast keccak`
    in rome-evm-private/program/src/non_evm/helper.rs:lazer_price_selector_locked).
  - Documentation captures the wrapper's wire-format guarantees +
    the consumer responsibilities (replay protection + freshness
    are consumer-side per spec §3.4).

`contracts/examples/lazer_consumer.sol` (new, ~80 LOC):
  - Reference consumer demonstrating the two consumer-side
    responsibilities the wrapper does NOT enforce:
      1. Per-feed monotonic replay protection via
         `mapping(uint32 => uint64) lastAcceptedPublishTimeUs`.
         (Multi-feed monotonicity is mandatory — a single envelope-
         level timestamp is INSUFFICIENT per spec §5.2.)
      2. Staleness enforcement with consumer-chosen MAX_STALENESS_US.
  - `getEthPrice` is NOT `view` because it mutates the per-feed
    monotonicity tracker — high-stakes use cases (lending
    liquidations, bridge value verification) MUST track in storage.
    Low-stakes UI display can do a pure-view variant without the
    monotonicity check.

## Spec compliance

Per `rome-specs/main/active/technical/2026-05-19-pyth-lazer-integration.md` §5:
  - Solidity surface matches §5.1's IHelperProgram extension
  - Consumer pattern matches §5.2's worked example
  - LazerFeedPrice struct matches §3.3.1's ABI-encoded shape
  - Selector matches §3.1's `cast keccak` derivation
  - Per-feed monotonicity callout matches §3.4 + §5.2's "multi-feed
    monotonicity is mandatory" prose

## Verification

  - `npx hardhat compile` → clean (46 files compiled, no new warnings
    on touched files; existing meteora-factory size warning is unrelated)

## Cross-repo coordination

This commit pairs with the rome-evm-private `lazer-pyth-integration`
branch which adds the on-chain wrapper at the dispatched selector. The
two MUST be merged in this order to avoid a Solidity surface that
points at an unimplemented selector:

  1. rome-evm-private/lazer-pyth-integration merges to master
  2. Hadrian (or any test chain) rebuilds with new program
  3. THIS PR (rome-solidity/lazer-pyth-integration) merges to master
  4. (optional) Contracts deploy the worked consumer for end-to-end smoke

If a chain runs rome-solidity master from this commit against a Rome
program that predates the Lazer wrapper, lazer_price calls revert with
`Unimplemented(method is not supported by HelperProgram 0xa5f15a86)` —
clear surface, no silent data corruption.

## Iron rule respected

One Lazer feature → one PR per repo. This is the rome-solidity-side
single PR (the rome-evm-private side is a separate PR in that repo).
Per [[feedback_multi_phase_features_one_pr]].
@anil-rome anil-rome merged commit 57b57ce into master May 19, 2026
9 checks passed
@anil-rome anil-rome deleted the lazer-pyth-integration branch May 19, 2026 17:12
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.

2 participants