Skip to content

Commit 3aed1cc

Browse files
feat(lazer/sui): state and initializer (#2972)
* feat(lazer/sui): specify version * feat(lazer-sui): add state module with trusted signer storage and tests Co-Authored-By: Tejas Badadare <[email protected]> * fix Co-Authored-By: Tejas Badadare <[email protected]> * fix(sui-lazer): finalize state module with trusted signer management and tests; verified with Sui CLI 1.53.2 Co-Authored-By: Tejas Badadare <[email protected]> * chore(sui-lazer): remove warnings in state module; clean build on Sui CLI 1.53.2 Co-Authored-By: Tejas Badadare <[email protected]> * feat(lazer/sui): add trusted signer state * feat(lazer/sui): add package init and AdminCapability; share State and gate updates with admin cap Co-Authored-By: Tejas Badadare <[email protected]> * comments * resolve warnings * feat(sui-lazer): add admin-gated entry to update trusted signer via AdminCapability Co-Authored-By: Tejas Badadare <[email protected]> * Revert "feat(sui-lazer): add admin-gated entry to update trusted signer via AdminCapability" This reverts commit 187f0e9. * naming, make update_trusted_signer public * feat(sui-lazer): use OTW for AdminCap in admin::init; add OTW to pyth_lazer::init; share State; add OTW doc comments Co-Authored-By: Tejas Badadare <[email protected]> * lint * doc * remove unneeded otw type check * lint --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 10368cc commit 3aed1cc

File tree

6 files changed

+262
-7
lines changed

6 files changed

+262
-7
lines changed

lazer/contracts/sui/Move.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[move]
44
version = 3
5-
manifest_digest = "DD0B86B0E012F788977D2224EA46B39395FCF48AB7DAE200E70E6E12F9445868"
5+
manifest_digest = "5B8FA4A1860DFE72BCB751FB8D867DA8D63CCFD7029F175B345B072433DFC568"
66
deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C"
77
dependencies = [
88
{ id = "Bridge", name = "Bridge" },

lazer/contracts/sui/Move.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
[package]
22
name = "pyth_lazer"
3+
version = "0.0.0"
34
edition = "2024.beta"
45

5-
[dependencies]
6-
76
[addresses]
87
pyth_lazer = "0x0"
98

lazer/contracts/sui/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
`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.
44

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

77
### Build, test, deploy
88

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module pyth_lazer::admin;
2+
use sui::types;
3+
4+
public struct AdminCap has key, store {
5+
id: UID,
6+
}
7+
8+
/// The `ADMIN` resource serves as the one-time witness.
9+
/// It has the `drop` ability, allowing it to be consumed immediately after use.
10+
/// See: https://move-book.com/programmability/one-time-witness
11+
public struct ADMIN has drop {}
12+
13+
14+
/// Initializes the module. Called at publish time.
15+
/// Creates and transfers ownership of the singular AdminCap capability to the deployer.
16+
/// Only the AdminCap owner can update the trusted signers.
17+
fun init(_: ADMIN, ctx: &mut TxContext) {
18+
let cap = AdminCap { id: object::new(ctx) };
19+
transfer::public_transfer(cap, tx_context::sender(ctx));
20+
}
21+
22+
#[test_only]
23+
public fun mint_for_test(ctx: &mut TxContext): AdminCap {
24+
AdminCap { id: object::new(ctx) }
25+
}
26+
27+
#[test_only]
28+
public fun destroy_for_test(cap: AdminCap) {
29+
let AdminCap { id } = cap;
30+
object::delete(id)
31+
}

lazer/contracts/sui/sources/pyth_lazer.move

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use pyth_lazer::i64::Self;
55
use pyth_lazer::update::{Self, Update};
66
use pyth_lazer::feed::{Self, Feed};
77
use pyth_lazer::channel::Self;
8+
use pyth_lazer::state;
89
use sui::bcs;
910
use sui::ecdsa_k1::secp256k1_ecrecover;
1011

@@ -14,12 +15,22 @@ const PAYLOAD_MAGIC: u32 = 2479346549;
1415

1516

1617
// TODO:
17-
// initializer
18-
// administration -> admin cap, upgrade cap, governance?
19-
// storage module -> trusted signers, update fee?, treasury?
2018
// error handling
2119
// standalone verify signature function
2220

21+
/// The `PYTH_LAZER` resource serves as the one-time witness.
22+
/// It has the `drop` ability, allowing it to be consumed immediately after use.
23+
/// See: https://move-book.com/programmability/one-time-witness
24+
public struct PYTH_LAZER has drop {}
25+
26+
/// Initializes the module. Called at publish time.
27+
/// Creates and shares the singular State object.
28+
/// AdminCap is created and transferred in admin::init via a One-Time Witness.
29+
fun init(_: PYTH_LAZER, ctx: &mut TxContext) {
30+
let s = state::new(ctx);
31+
transfer::public_share_object(s);
32+
}
33+
2334
/// Parse the Lazer update message and validate the signature.
2435
///
2536
/// The parsing logic is based on the Lazer rust protocol definition defined here:
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
module pyth_lazer::state;
2+
3+
use pyth_lazer::admin::AdminCap;
4+
use pyth_lazer::admin;
5+
6+
const ED25519_PUBKEY_LEN: u64 = 32;
7+
const EInvalidPubkeyLen: u64 = 1;
8+
const ESignerNotFound: u64 = 2;
9+
10+
11+
/// Lazer State consists of the current set of trusted signers.
12+
/// By verifying that a price update was signed by one of these public keys,
13+
/// you can validate the authenticity of a Lazer price update.
14+
///
15+
/// The trusted signers are subject to rotations and expiry.
16+
public struct State has key, store {
17+
id: UID,
18+
trusted_signers: vector<TrustedSignerInfo>,
19+
}
20+
21+
/// A trusted signer is comprised of a pubkey and an expiry time.
22+
/// A signer's signature should only be trusted up to timestamp `expires_at`.
23+
public struct TrustedSignerInfo has copy, drop, store {
24+
public_key: vector<u8>,
25+
expires_at: u64,
26+
}
27+
28+
public(package) fun new(ctx: &mut TxContext): State {
29+
State {
30+
id: object::new(ctx),
31+
trusted_signers: vector::empty<TrustedSignerInfo>(),
32+
}
33+
}
34+
35+
/// Get the trusted signer's public key
36+
public fun public_key(info: &TrustedSignerInfo): &vector<u8> {
37+
&info.public_key
38+
}
39+
40+
/// Get the trusted signer's expiry timestamp
41+
public fun expires_at(info: &TrustedSignerInfo): u64 {
42+
info.expires_at
43+
}
44+
45+
/// Get the list of trusted signers
46+
public fun get_trusted_signers(s: &State): &vector<TrustedSignerInfo> {
47+
&s.trusted_signers
48+
}
49+
50+
/// Upsert a trusted signer's information or remove them. Can only be called by the AdminCap holder.
51+
/// - If the trusted signer pubkey already exists, the expires_at will be updated.
52+
/// - If the expired_at is set to zero, the trusted signer will be removed.
53+
/// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at.
54+
public fun update_trusted_signer(_: &AdminCap, s: &mut State, pubkey: vector<u8>, expires_at: u64) {
55+
assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, EInvalidPubkeyLen);
56+
57+
let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey);
58+
if (expires_at == 0) {
59+
if (option::is_some(&maybe_idx)) {
60+
let idx = option::extract(&mut maybe_idx);
61+
// Remove by swapping with last (order not preserved), discard removed value
62+
let _ = vector::swap_remove(&mut s.trusted_signers, idx);
63+
} else {
64+
option::destroy_none(maybe_idx);
65+
abort ESignerNotFound
66+
};
67+
return
68+
};
69+
70+
if (option::is_some(&maybe_idx)) {
71+
let idx = option::extract(&mut maybe_idx);
72+
let info_ref = vector::borrow_mut(&mut s.trusted_signers, idx);
73+
info_ref.expires_at = expires_at
74+
} else {
75+
option::destroy_none(maybe_idx);
76+
vector::push_back(
77+
&mut s.trusted_signers,
78+
TrustedSignerInfo { public_key: pubkey, expires_at },
79+
)
80+
}
81+
}
82+
83+
fun find_signer_index(signers: &vector<TrustedSignerInfo>, target: &vector<u8>): Option<u64> {
84+
let len = vector::length(signers);
85+
let mut i: u64 = 0;
86+
while (i < (len as u64)) {
87+
let info_ref = vector::borrow(signers, i);
88+
if (*public_key(info_ref) == *target) {
89+
return option::some(i)
90+
};
91+
i = i + 1
92+
};
93+
option::none()
94+
}
95+
96+
#[test_only]
97+
public fun new_for_test(ctx: &mut TxContext): State {
98+
State {
99+
id: object::new(ctx),
100+
trusted_signers: vector::empty<TrustedSignerInfo>(),
101+
}
102+
}
103+
104+
#[test]
105+
public fun test_add_new_signer() {
106+
let mut ctx = tx_context::dummy();
107+
let mut s = new_for_test(&mut ctx);
108+
let admin_cap = admin::mint_for_test(&mut ctx);
109+
110+
let pk = x"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
111+
let expiry: u64 = 123;
112+
113+
update_trusted_signer(&admin_cap, &mut s, pk, expiry);
114+
115+
let signers_ref = get_trusted_signers(&s);
116+
assert!(vector::length(signers_ref) == 1, 100);
117+
let info = vector::borrow(signers_ref, 0);
118+
assert!(expires_at(info) == 123, 101);
119+
let got_pk = public_key(info);
120+
assert!(vector::length(got_pk) == (ED25519_PUBKEY_LEN as u64), 102);
121+
let State { id, trusted_signers } = s;
122+
let _ = trusted_signers;
123+
object::delete(id);
124+
admin::destroy_for_test(admin_cap);
125+
}
126+
127+
#[test]
128+
public fun test_update_existing_signer_expiry() {
129+
let mut ctx = tx_context::dummy();
130+
let mut s = new_for_test(&mut ctx);
131+
let admin_cap = admin::mint_for_test(&mut ctx);
132+
133+
update_trusted_signer(
134+
&admin_cap,
135+
&mut s,
136+
x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
137+
1000,
138+
);
139+
update_trusted_signer(
140+
&admin_cap,
141+
&mut s,
142+
x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
143+
2000,
144+
);
145+
146+
let signers_ref = get_trusted_signers(&s);
147+
assert!(vector::length(signers_ref) == 1, 110);
148+
let info = vector::borrow(signers_ref, 0);
149+
assert!(expires_at(info) == 2000, 111);
150+
let State { id, trusted_signers } = s;
151+
let _ = trusted_signers;
152+
object::delete(id);
153+
admin::destroy_for_test(admin_cap);
154+
}
155+
156+
#[test]
157+
public fun test_remove_signer_by_zero_expiry() {
158+
let mut ctx = tx_context::dummy();
159+
let mut s = new_for_test(&mut ctx);
160+
let admin_cap = admin::mint_for_test(&mut ctx);
161+
162+
update_trusted_signer(
163+
&admin_cap,
164+
&mut s,
165+
x"0707070707070707070707070707070707070707070707070707070707070707",
166+
999,
167+
);
168+
update_trusted_signer(
169+
&admin_cap,
170+
&mut s,
171+
x"0707070707070707070707070707070707070707070707070707070707070707",
172+
0,
173+
);
174+
175+
let signers_ref = get_trusted_signers(&s);
176+
assert!(vector::length(signers_ref) == 0, 120);
177+
let State { id, trusted_signers } = s;
178+
let _ = trusted_signers;
179+
object::delete(id);
180+
admin::destroy_for_test(admin_cap);
181+
}
182+
183+
#[test, expected_failure(abort_code = EInvalidPubkeyLen)]
184+
public fun test_invalid_pubkey_length_rejected() {
185+
let mut ctx = tx_context::dummy();
186+
let mut s = new_for_test(&mut ctx);
187+
let admin_cap = admin::mint_for_test(&mut ctx);
188+
189+
let short_pk = x"010203";
190+
update_trusted_signer(&admin_cap, &mut s, short_pk, 1);
191+
let State { id, trusted_signers } = s;
192+
let _ = trusted_signers;
193+
object::delete(id);
194+
admin::destroy_for_test(admin_cap);
195+
}
196+
197+
#[test, expected_failure(abort_code = ESignerNotFound)]
198+
public fun test_remove_nonexistent_signer_fails() {
199+
let mut ctx = tx_context::dummy();
200+
let mut s = new_for_test(&mut ctx);
201+
let admin_cap = admin::mint_for_test(&mut ctx);
202+
203+
// Try to remove a signer that doesn't exist by setting expires_at to 0
204+
update_trusted_signer(
205+
&admin_cap,
206+
&mut s,
207+
x"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
208+
0,
209+
);
210+
let State { id, trusted_signers } = s;
211+
let _ = trusted_signers;
212+
object::delete(id);
213+
admin::destroy_for_test(admin_cap);
214+
}

0 commit comments

Comments
 (0)