Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2c4ae46
feat(lazer/sui): specify version
tejasbadadare Aug 19, 2025
69682bc
feat(lazer-sui): add state module with trusted signer storage and tests
devin-ai-integration[bot] Aug 19, 2025
21d8937
Merge branch 'tb/lazer/sui-storage' of github.com:pyth-network/pyth-c…
tejasbadadare Aug 19, 2025
7380293
fix
devin-ai-integration[bot] Aug 19, 2025
0e70796
Merge branch 'tb/lazer/sui-storage' of github.com:pyth-network/pyth-c…
tejasbadadare Aug 19, 2025
f34b7fc
fix(sui-lazer): finalize state module with trusted signer management …
devin-ai-integration[bot] Aug 19, 2025
112cd39
Merge branch 'tb/lazer/sui-storage' of github.com:pyth-network/pyth-c…
tejasbadadare Aug 19, 2025
d7bd862
chore(sui-lazer): remove warnings in state module; clean build on Sui…
devin-ai-integration[bot] Aug 19, 2025
48e070d
feat(lazer/sui): add trusted signer state
tejasbadadare Aug 19, 2025
8ee64ba
feat(lazer/sui): add package init and AdminCapability; share State an…
devin-ai-integration[bot] Aug 19, 2025
a7436fa
comments
tejasbadadare Aug 19, 2025
88ef465
Merge branch 'tb/lazer/sui-storage' of github.com:pyth-network/pyth-c…
tejasbadadare Aug 19, 2025
e64eb21
resolve warnings
tejasbadadare Aug 19, 2025
187f0e9
feat(sui-lazer): add admin-gated entry to update trusted signer via A…
devin-ai-integration[bot] Aug 19, 2025
ddad6d6
Revert "feat(sui-lazer): add admin-gated entry to update trusted sign…
devin-ai-integration[bot] Aug 19, 2025
0fc417e
naming, make update_trusted_signer public
tejasbadadare Aug 20, 2025
da1a2a1
feat(sui-lazer): use OTW for AdminCap in admin::init; add OTW to pyth…
devin-ai-integration[bot] Aug 20, 2025
389f5d7
lint
tejasbadadare Aug 20, 2025
f1b6fc4
doc
tejasbadadare Aug 20, 2025
96eaa10
feat(lazer/sui): verify against trusted signers
tejasbadadare Aug 21, 2025
f53e83f
docs, clean up
tejasbadadare Aug 21, 2025
b9a844d
remove unneeded otw type check
tejasbadadare Aug 22, 2025
9c92f6e
Merge branch 'tb/lazer/sui-storage' of github.com:pyth-network/pyth-c…
tejasbadadare Aug 22, 2025
41c66dc
lint
tejasbadadare Aug 22, 2025
7f10b9d
Merge branch 'tb/lazer/sui-storage' of github.com:pyth-network/pyth-c…
tejasbadadare Aug 22, 2025
d8028d1
max verify_le_ecdsa_message package private
tejasbadadare Aug 25, 2025
54ad564
Merge branch 'main' of github.com:pyth-network/pyth-crosschain into t…
tejasbadadare Aug 25, 2025
049363c
fix imports
tejasbadadare Aug 25, 2025
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
2 changes: 1 addition & 1 deletion lazer/contracts/sui/Move.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[move]
version = 3
manifest_digest = "DD0B86B0E012F788977D2224EA46B39395FCF48AB7DAE200E70E6E12F9445868"
manifest_digest = "5B8FA4A1860DFE72BCB751FB8D867DA8D63CCFD7029F175B345B072433DFC568"
deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C"
dependencies = [
{ id = "Bridge", name = "Bridge" },
Expand Down
3 changes: 1 addition & 2 deletions lazer/contracts/sui/Move.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
[package]
name = "pyth_lazer"
version = "0.0.0"
edition = "2024.beta"

[dependencies]

[addresses]
pyth_lazer = "0x0"

Expand Down
2 changes: 1 addition & 1 deletion lazer/contracts/sui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`pyth_lazer` is a Sui package that allows consumers to easily parse and verify cryptographically signed price feed data from the Pyth Network's high-frequency Lazer protocol for use on-chain.

This package is built using the Move language and Sui framework.
This package is built using the Move language edition `2024.beta` and Sui framework `v1.53.2`.

### Build, test, deploy

Expand Down
30 changes: 30 additions & 0 deletions lazer/contracts/sui/sources/admin.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module pyth_lazer::admin;

public struct AdminCap has key, store {
id: UID,
}

/// The `ADMIN` resource serves as the one-time witness.
/// It has the `drop` ability, allowing it to be consumed immediately after use.
/// See: https://move-book.com/programmability/one-time-witness
public struct ADMIN has drop {}


/// Initializes the module. Called at publish time.
/// Creates and transfers ownership of the singular AdminCap capability to the deployer.
/// Only the AdminCap owner can update the trusted signers.
fun init(_: ADMIN, ctx: &mut TxContext) {
let cap = AdminCap { id: object::new(ctx) };
transfer::public_transfer(cap, tx_context::sender(ctx));
}

#[test_only]
public fun mint_for_test(ctx: &mut TxContext): AdminCap {
AdminCap { id: object::new(ctx) }
}

#[test_only]
public fun destroy_for_test(cap: AdminCap) {
let AdminCap { id } = cap;
object::delete(id)
}
111 changes: 89 additions & 22 deletions lazer/contracts/sui/sources/pyth_lazer.move
Original file line number Diff line number Diff line change
@@ -1,32 +1,96 @@
module pyth_lazer::pyth_lazer;

use pyth_lazer::i16::Self;
use pyth_lazer::i64::Self;
use pyth_lazer::update::{Self, Update};
use pyth_lazer::channel;
use pyth_lazer::feed::{Self, Feed};
use pyth_lazer::channel::Self;
use pyth_lazer::i16;
use pyth_lazer::i64;
use pyth_lazer::state::{Self, State};
use pyth_lazer::update::{Self, Update};
use sui::bcs;
use sui::ecdsa_k1::secp256k1_ecrecover;
use sui::clock::Clock;

const SECP256K1_SIG_LEN: u32 = 65;
const UPDATE_MESSAGE_MAGIC: u32 = 1296547300;
const PAYLOAD_MAGIC: u32 = 2479346549;

// Error codes
const EInvalidUpdate: u64 = 1;
const ESignerNotTrusted: u64 = 2;
const ESignerExpired: u64 = 3;

// TODO:
// initializer
// administration -> admin cap, upgrade cap, governance?
// storage module -> trusted signers, update fee?, treasury?
// error handling
// standalone verify signature function

/// Parse the Lazer update message and validate the signature.
/// The `PYTH_LAZER` resource serves as the one-time witness.
/// It has the `drop` ability, allowing it to be consumed immediately after use.
/// See: https://move-book.com/programmability/one-time-witness
public struct PYTH_LAZER has drop {}

/// Initializes the module. Called at publish time.
/// Creates and shares the singular State object.
/// AdminCap is created and transferred in admin::init via a One-Time Witness.
fun init(_: PYTH_LAZER, ctx: &mut TxContext) {
let s = state::new(ctx);
transfer::public_share_object(s);
}

/// Verify LE ECDSA message signature against trusted signers.
///
/// This function recovers the public key from the signature and payload,
/// then checks if the recovered public key is in the trusted signers list
/// and has not expired.
///
/// # Arguments
/// * `s` - The pyth_lazer::state::State
/// * `clock` - The sui::clock::Clock
/// * `signature` - The ECDSA signature bytes (little endian)
/// * `payload` - The message payload that was signed
///
/// # Errors
/// * `ESignerNotTrusted` - The recovered public key is not in the trusted signers list
/// * `ESignerExpired` - The signer's certificate has expired
public fun verify_le_ecdsa_message(
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need to make it public :? maybe public in crate be enough (so we can test it, but users don't use it)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, figured it might be handy for users, but yeah i can't think of a scenario where they'd wanna validate without parsing.

s: &State,
clock: &Clock,
signature: &vector<u8>,
payload: &vector<u8>,
) {
// 0 stands for keccak256 hash
let pubkey = secp256k1_ecrecover(signature, payload, 0);

// Check if the recovered pubkey is in the trusted signers list
let trusted_signers = state::get_trusted_signers(s);
let mut maybe_idx = state::find_signer_index(trusted_signers, &pubkey);

if (option::is_some(&maybe_idx)) {
let idx = option::extract(&mut maybe_idx);
let found_signer = &trusted_signers[idx];
let expires_at = state::expires_at(found_signer);
assert!(clock.timestamp_ms() < expires_at, ESignerExpired);
} else {
abort ESignerNotTrusted
}
}

/// Parse the Lazer update message and validate the signature within.
/// The parsing logic is based on the Lazer rust protocol definition defined here:
/// https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/sdk/rust/protocol
public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
///
/// # Arguments
/// * `s` - The pyth_lazer::state::State
/// * `clock` - The sui::clock::Clock
/// * `update` - The LeEcdsa formatted Lazer update
///
/// # Errors
/// * `EInvalidUpdate` - Failed to parse the update according to the protocol definition
/// * `ESignerNotTrusted` - The recovered public key is not in the trusted signers list
public fun parse_and_verify_le_ecdsa_update(s: &State, clock: &Clock, update: vector<u8>): Update {
let mut cursor = bcs::new(update);

// TODO: introduce helper functions to check data len before peeling. allows us to return more
// granular error messages.

let magic = cursor.peel_u32();
assert!(magic == UPDATE_MESSAGE_MAGIC, 0);

Expand All @@ -44,18 +108,15 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {

assert!((payload_len as u64) == payload.length(), 0);

// 0 stands for keccak256 hash
let pubkey = secp256k1_ecrecover(&signature, &payload, 0);

// Lazer signer pubkey
// FIXME: validate against trusted signer set in storage
assert!(pubkey == x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b", 0);

let mut cursor = bcs::new(payload);
let payload_magic = cursor.peel_u32();
assert!(payload_magic == PAYLOAD_MAGIC, 0);

let timestamp = cursor.peel_u64();

// Verify the signature against trusted signers
verify_le_ecdsa_message(s, clock, &signature, &payload);

let channel_value = cursor.peel_u8();
let channel = if (channel_value == 0) {
channel::new_invalid()
Expand Down Expand Up @@ -86,7 +147,7 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
option::none(),
option::none(),
option::none(),
option::none()
option::none(),
);

let properties_count = cursor.peel_u8();
Expand All @@ -105,14 +166,18 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
} else if (property_id == 1) {
let best_bid_price = cursor.peel_u64();
if (best_bid_price != 0) {
feed.set_best_bid_price(option::some(option::some(i64::from_u64(best_bid_price))));
feed.set_best_bid_price(
option::some(option::some(i64::from_u64(best_bid_price))),
);
} else {
feed.set_best_bid_price(option::some(option::none()));
}
} else if (property_id == 2) {
let best_ask_price = cursor.peel_u64();
if (best_ask_price != 0) {
feed.set_best_ask_price(option::some(option::some(i64::from_u64(best_ask_price))));
feed.set_best_ask_price(
option::some(option::some(i64::from_u64(best_ask_price))),
);
} else {
feed.set_best_ask_price(option::some(option::none()));
}
Expand Down Expand Up @@ -151,14 +216,16 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {

if (exists == 1) {
let funding_rate_interval = cursor.peel_u64();
feed.set_funding_rate_interval(option::some(option::some(funding_rate_interval)));
feed.set_funding_rate_interval(
option::some(option::some(funding_rate_interval)),
);
} else {
feed.set_funding_rate_interval(option::some(option::none()));
}
} else {
// When we have an unknown property, we do not know its length, and therefore
// we cannot ignore it and parse the next properties.
abort 0 // FIXME: return more granular error messages
abort EInvalidUpdate // FIXME: return more granular error messages
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this no longer a fixme?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still a FIXME since there are several other places in the parsing where the code can revert. Will be tackling this next.

};

properties_i = properties_i + 1;
Expand Down
Loading