1
1
module pyth_lazer ::pyth_lazer ;
2
2
3
- use pyth_lazer::i16::Self ;
4
- use pyth_lazer::i64::Self ;
5
- use pyth_lazer::update::{Self , Update };
3
+ use pyth_lazer::channel;
6
4
use pyth_lazer::feed::{Self , Feed };
7
- use pyth_lazer::channel::Self ;
8
- use pyth_lazer::state;
5
+ use pyth_lazer::state::{Self , State };
6
+ use pyth_lazer::i64::{Self };
7
+ use pyth_lazer::i16::{Self };
8
+ use pyth_lazer::update::{Self , Update };
9
9
use sui::bcs;
10
+ use sui::clock::Clock ;
10
11
use sui::ecdsa_k1::secp256k1_ecrecover;
11
12
12
13
const SECP256K1_SIG_LEN : u32 = 65 ;
13
14
const UPDATE_MESSAGE_MAGIC : u32 = 1296547300 ;
14
15
const PAYLOAD_MAGIC : u32 = 2479346549 ;
15
16
17
+ // Error codes
18
+ const EInvalidUpdate : u64 = 1 ;
19
+ const ESignerNotTrusted : u64 = 2 ;
20
+ const ESignerExpired : u64 = 3 ;
16
21
17
22
// TODO:
18
23
// error handling
19
- // standalone verify signature function
20
24
21
25
/// The `PYTH_LAZER` resource serves as the one-time witness.
22
26
/// It has the `drop` ability, allowing it to be consumed immediately after use.
@@ -31,13 +35,62 @@ fun init(_: PYTH_LAZER, ctx: &mut TxContext) {
31
35
transfer::public_share_object (s);
32
36
}
33
37
34
- /// Parse the Lazer update message and validate the signature.
38
+ /// Verify LE ECDSA message signature against trusted signers.
39
+ ///
40
+ /// This function recovers the public key from the signature and payload,
41
+ /// then checks if the recovered public key is in the trusted signers list
42
+ /// and has not expired.
35
43
///
44
+ /// # Arguments
45
+ /// * `s` - The pyth_lazer::state::State
46
+ /// * `clock` - The sui::clock::Clock
47
+ /// * `signature` - The ECDSA signature bytes (little endian)
48
+ /// * `payload` - The message payload that was signed
49
+ ///
50
+ /// # Errors
51
+ /// * `ESignerNotTrusted` - The recovered public key is not in the trusted signers list
52
+ /// * `ESignerExpired` - The signer's certificate has expired
53
+ public (package ) fun verify_le_ecdsa_message (
54
+ s: &State ,
55
+ clock: &Clock ,
56
+ signature: &vector <u8 >,
57
+ payload: &vector <u8 >,
58
+ ) {
59
+ // 0 stands for keccak256 hash
60
+ let pubkey = secp256k1_ecrecover (signature, payload, 0 );
61
+
62
+ // Check if the recovered pubkey is in the trusted signers list
63
+ let trusted_signers = state::get_trusted_signers (s);
64
+ let mut maybe_idx = state::find_signer_index (trusted_signers, &pubkey);
65
+
66
+ if (option::is_some (&maybe_idx)) {
67
+ let idx = option::extract (&mut maybe_idx);
68
+ let found_signer = &trusted_signers[idx];
69
+ let expires_at = state::expires_at (found_signer);
70
+ assert !(clock.timestamp_ms () < expires_at, ESignerExpired );
71
+ } else {
72
+ abort ESignerNotTrusted
73
+ }
74
+ }
75
+
76
+ /// Parse the Lazer update message and validate the signature within.
36
77
/// The parsing logic is based on the Lazer rust protocol definition defined here:
37
78
/// https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/sdk/rust/protocol
38
- public fun parse_and_verify_le_ecdsa_update (update: vector <u8 >): Update {
79
+ ///
80
+ /// # Arguments
81
+ /// * `s` - The pyth_lazer::state::State
82
+ /// * `clock` - The sui::clock::Clock
83
+ /// * `update` - The LeEcdsa formatted Lazer update
84
+ ///
85
+ /// # Errors
86
+ /// * `EInvalidUpdate` - Failed to parse the update according to the protocol definition
87
+ /// * `ESignerNotTrusted` - The recovered public key is not in the trusted signers list
88
+ public fun parse_and_verify_le_ecdsa_update (s: &State , clock: &Clock , update: vector <u8 >): Update {
39
89
let mut cursor = bcs::new (update);
40
90
91
+ // TODO: introduce helper functions to check data len before peeling. allows us to return more
92
+ // granular error messages.
93
+
41
94
let magic = cursor.peel_u32 ();
42
95
assert !(magic == UPDATE_MESSAGE_MAGIC , 0 );
43
96
@@ -55,18 +108,15 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
55
108
56
109
assert !((payload_len as u64 ) == payload.length (), 0 );
57
110
58
- // 0 stands for keccak256 hash
59
- let pubkey = secp256k1_ecrecover (&signature, &payload, 0 );
60
-
61
- // Lazer signer pubkey
62
- // FIXME: validate against trusted signer set in storage
63
- assert !(pubkey == x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b ", 0 );
64
-
65
111
let mut cursor = bcs::new (payload);
66
112
let payload_magic = cursor.peel_u32 ();
67
113
assert !(payload_magic == PAYLOAD_MAGIC , 0 );
68
114
69
115
let timestamp = cursor.peel_u64 ();
116
+
117
+ // Verify the signature against trusted signers
118
+ verify_le_ecdsa_message (s, clock, &signature, &payload);
119
+
70
120
let channel_value = cursor.peel_u8 ();
71
121
let channel = if (channel_value == 0 ) {
72
122
channel::new_invalid ()
@@ -97,7 +147,7 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
97
147
option::none (),
98
148
option::none (),
99
149
option::none (),
100
- option::none ()
150
+ option::none (),
101
151
);
102
152
103
153
let properties_count = cursor.peel_u8 ();
@@ -116,14 +166,18 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
116
166
} else if (property_id == 1 ) {
117
167
let best_bid_price = cursor.peel_u64 ();
118
168
if (best_bid_price != 0 ) {
119
- feed.set_best_bid_price (option::some (option::some (i64::from_u64 (best_bid_price))));
169
+ feed.set_best_bid_price (
170
+ option::some (option::some (i64::from_u64 (best_bid_price))),
171
+ );
120
172
} else {
121
173
feed.set_best_bid_price (option::some (option::none ()));
122
174
}
123
175
} else if (property_id == 2 ) {
124
176
let best_ask_price = cursor.peel_u64 ();
125
177
if (best_ask_price != 0 ) {
126
- feed.set_best_ask_price (option::some (option::some (i64::from_u64 (best_ask_price))));
178
+ feed.set_best_ask_price (
179
+ option::some (option::some (i64::from_u64 (best_ask_price))),
180
+ );
127
181
} else {
128
182
feed.set_best_ask_price (option::some (option::none ()));
129
183
}
@@ -162,14 +216,16 @@ public fun parse_and_verify_le_ecdsa_update(update: vector<u8>): Update {
162
216
163
217
if (exists == 1 ) {
164
218
let funding_rate_interval = cursor.peel_u64 ();
165
- feed.set_funding_rate_interval (option::some (option::some (funding_rate_interval)));
219
+ feed.set_funding_rate_interval (
220
+ option::some (option::some (funding_rate_interval)),
221
+ );
166
222
} else {
167
223
feed.set_funding_rate_interval (option::some (option::none ()));
168
224
}
169
225
} else {
170
226
// When we have an unknown property, we do not know its length, and therefore
171
227
// we cannot ignore it and parse the next properties.
172
- abort 0 // FIXME: return more granular error messages
228
+ abort EInvalidUpdate // FIXME: return more granular error messages
173
229
};
174
230
175
231
properties_i = properties_i + 1 ;
0 commit comments