|
| 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