diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index ab20d4492e..4daa3197de 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -1,5 +1,4 @@ module pyth_lazer::admin; -use sui::types; public struct AdminCap has key, store { id: UID, @@ -10,7 +9,6 @@ public struct AdminCap has key, store { /// 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. diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 102518d7a7..b10413c382 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -1,22 +1,26 @@ 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::state; +use pyth_lazer::state::{Self, State}; +use pyth_lazer::i64::{Self}; +use pyth_lazer::i16::{Self}; +use pyth_lazer::update::{Self, Update}; use sui::bcs; +use sui::clock::Clock; use sui::ecdsa_k1::secp256k1_ecrecover; 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: // error handling -// standalone verify signature function /// The `PYTH_LAZER` resource serves as the one-time witness. /// It has the `drop` ability, allowing it to be consumed immediately after use. @@ -31,13 +35,62 @@ fun init(_: PYTH_LAZER, ctx: &mut TxContext) { transfer::public_share_object(s); } -/// Parse the Lazer update message and validate the signature. +/// 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(package) fun verify_le_ecdsa_message( + s: &State, + clock: &Clock, + signature: &vector, + payload: &vector, +) { + // 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): 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): 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); @@ -55,18 +108,15 @@ public fun parse_and_verify_le_ecdsa_update(update: vector): 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() @@ -97,7 +147,7 @@ public fun parse_and_verify_le_ecdsa_update(update: vector): Update { option::none(), option::none(), option::none(), - option::none() + option::none(), ); let properties_count = cursor.peel_u8(); @@ -116,14 +166,18 @@ public fun parse_and_verify_le_ecdsa_update(update: vector): 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())); } @@ -162,14 +216,16 @@ public fun parse_and_verify_le_ecdsa_update(update: vector): 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 }; properties_i = properties_i + 1; diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index a2c351efbb..23f86b1e3d 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,13 +1,11 @@ module pyth_lazer::state; -use pyth_lazer::admin::AdminCap; -use pyth_lazer::admin; +use pyth_lazer::admin::{Self, AdminCap}; -const ED25519_PUBKEY_LEN: u64 = 32; +const SECP256K1_COMPRESSED_PUBKEY_LEN: u64 = 33; const EInvalidPubkeyLen: u64 = 1; const ESignerNotFound: u64 = 2; - /// Lazer State consists of the current set of trusted signers. /// By verifying that a price update was signed by one of these public keys, /// you can validate the authenticity of a Lazer price update. @@ -18,7 +16,7 @@ public struct State has key, store { trusted_signers: vector, } -/// A trusted signer is comprised of a pubkey and an expiry time. +/// A trusted signer is comprised of a pubkey and an expiry timestamp (seconds since Unix epoch). /// A signer's signature should only be trusted up to timestamp `expires_at`. public struct TrustedSignerInfo has copy, drop, store { public_key: vector, @@ -37,7 +35,7 @@ public fun public_key(info: &TrustedSignerInfo): &vector { &info.public_key } -/// Get the trusted signer's expiry timestamp +/// Get the trusted signer's expiry timestamp (seconds since Unix epoch) public fun expires_at(info: &TrustedSignerInfo): u64 { info.expires_at } @@ -52,7 +50,7 @@ public fun get_trusted_signers(s: &State): &vector { /// - If the expired_at is set to zero, the trusted signer will be removed. /// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at. public fun update_trusted_signer(_: &AdminCap, s: &mut State, pubkey: vector, expires_at: u64) { - assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, EInvalidPubkeyLen); + assert!(vector::length(&pubkey) as u64 == SECP256K1_COMPRESSED_PUBKEY_LEN, EInvalidPubkeyLen); let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); if (expires_at == 0) { @@ -80,12 +78,15 @@ public fun update_trusted_signer(_: &AdminCap, s: &mut State, pubkey: vector } } -fun find_signer_index(signers: &vector, target: &vector): Option { +public fun find_signer_index( + signers: &vector, + public_key: &vector, +): Option { let len = vector::length(signers); let mut i: u64 = 0; while (i < (len as u64)) { let info_ref = vector::borrow(signers, i); - if (*public_key(info_ref) == *target) { + if (*public_key(info_ref) == *public_key) { return option::some(i) }; i = i + 1 @@ -101,13 +102,20 @@ public fun new_for_test(ctx: &mut TxContext): State { } } +#[test_only] +public fun destroy_for_test(s: State) { + let State { id, trusted_signers } = s; + let _ = trusted_signers; + object::delete(id); +} + #[test] public fun test_add_new_signer() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); let admin_cap = admin::mint_for_test(&mut ctx); - let pk = x"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + let pk = x"030102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; let expiry: u64 = 123; update_trusted_signer(&admin_cap, &mut s, pk, expiry); @@ -117,7 +125,7 @@ public fun test_add_new_signer() { let info = vector::borrow(signers_ref, 0); assert!(expires_at(info) == 123, 101); let got_pk = public_key(info); - assert!(vector::length(got_pk) == (ED25519_PUBKEY_LEN as u64), 102); + assert!(vector::length(got_pk) == (SECP256K1_COMPRESSED_PUBKEY_LEN as u64), 102); let State { id, trusted_signers } = s; let _ = trusted_signers; object::delete(id); @@ -133,13 +141,13 @@ public fun test_update_existing_signer_expiry() { update_trusted_signer( &admin_cap, &mut s, - x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + x"032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000, ); update_trusted_signer( &admin_cap, &mut s, - x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + x"032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000, ); @@ -162,13 +170,13 @@ public fun test_remove_signer_by_zero_expiry() { update_trusted_signer( &admin_cap, &mut s, - x"0707070707070707070707070707070707070707070707070707070707070707", + x"030707070707070707070707070707070707070707070707070707070707070707", 999, ); update_trusted_signer( &admin_cap, &mut s, - x"0707070707070707070707070707070707070707070707070707070707070707", + x"030707070707070707070707070707070707070707070707070707070707070707", 0, ); @@ -204,7 +212,7 @@ public fun test_remove_nonexistent_signer_fails() { update_trusted_signer( &admin_cap, &mut s, - x"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + x"03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 0, ); let State { id, trusted_signers } = s; diff --git a/lazer/contracts/sui/tests/pyth_lazer_tests.move b/lazer/contracts/sui/tests/pyth_lazer_tests.move index e7de7294e2..5d3a20f5be 100644 --- a/lazer/contracts/sui/tests/pyth_lazer_tests.move +++ b/lazer/contracts/sui/tests/pyth_lazer_tests.move @@ -1,61 +1,78 @@ #[test_only] +#[allow(implicit_const_copy)] module pyth_lazer::pyth_lazer_tests; -use pyth_lazer::pyth_lazer::parse_and_verify_le_ecdsa_update; + +use pyth_lazer::admin; use pyth_lazer::channel::new_fixed_rate_200ms; -use pyth_lazer::i16::{Self}; -use pyth_lazer::i64::{Self}; +use pyth_lazer::i16; +use pyth_lazer::i64; +use pyth_lazer::pyth_lazer::{parse_and_verify_le_ecdsa_update, verify_le_ecdsa_message, ESignerNotTrusted, ESignerExpired}; +use pyth_lazer::state; +use sui::clock; + +const TEST_LAZER_UPDATE: vector = x"e4bd474d42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101a10075d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000"; +const TEST_PAYLOAD: vector = x"75d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000"; +const TEST_SIGNATURE: vector = x"42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101"; +const TEST_TRUSTED_SIGNER_PUBKEY: vector = x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b"; + +/* +The test data above is from the Lazer subscription: +> Request +{"subscriptionId": 1, "type": "subscribe", "priceFeedIds": [1, 2, 112], "properties": ["price", "bestBidPrice", "bestAskPrice", "exponent", "fundingRate", "fundingTimestamp", "fundingRateInterval"], "chains": ["leEcdsa"], "channel": "fixed_rate@200ms", "jsonBinaryEncoding": "hex"} +< Response +{ + "type": "streamUpdated", + "subscriptionId": 1, + "parsed": { + "timestampUs": "1755625313400000", + "priceFeeds": [ + { + "priceFeedId": 1, + "price": "11350721594969", + "bestBidPrice": "11350696257890", + "bestAskPrice": "11350868428965", + "exponent": -8 + }, + { + "priceFeedId": 2, + "price": "417775510136", + "bestBidPrice": "417771266475", + "bestAskPrice": "417782074042", + "exponent": -8 + }, + { + "priceFeedId": 112, + "price": "113747064619385816", + "exponent": -12, + "fundingRate": 31670000, + "fundingTimestamp": 1755619200000000, + "fundingRateInterval": 28800000000 + } + ] + }, + "leEcdsa": { + "encoding": "hex", + "data": "e4bd474d42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101a10075d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000" + } +} +*/ #[test] public fun test_parse_and_verify_le_ecdsa_update() { - /* - The test data is from the Lazer subscription: - > Request - {"subscriptionId": 1, "type": "subscribe", "priceFeedIds": [1, 2, 112], "properties": ["price", "bestBidPrice", "bestAskPrice", "exponent", "fundingRate", "fundingTimestamp", "fundingRateInterval"], "chains": ["leEcdsa"], "channel": "fixed_rate@200ms", "jsonBinaryEncoding": "hex"} - < Response - { - "type": "streamUpdated", - "subscriptionId": 1, - "parsed": { - "timestampUs": "1755625313400000", - "priceFeeds": [ - { - "priceFeedId": 1, - "price": "11350721594969", - "bestBidPrice": "11350696257890", - "bestAskPrice": "11350868428965", - "exponent": -8 - }, - { - "priceFeedId": 2, - "price": "417775510136", - "bestBidPrice": "417771266475", - "bestAskPrice": "417782074042", - "exponent": -8 - }, - { - "priceFeedId": 112, - "price": "113747064619385816", - "exponent": -12, - "fundingRate": 31670000, - "fundingTimestamp": 1755619200000000, - "fundingRateInterval": 28800000000 - } - ] - }, - "leEcdsa": { - "encoding": "hex", - "data": "e4bd474d42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101a10075d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000" - } - } - */ + let mut ctx = tx_context::dummy(); + let mut s = state::new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); + let clock = clock::create_for_testing(&mut ctx); - let hex_message = - x"e4bd474d42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101a10075d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000"; + // Add the trusted signer that matches the test data + let trusted_pubkey = TEST_TRUSTED_SIGNER_PUBKEY; + let expiry_time = 2000000000000000; // Far in the future + state::update_trusted_signer(&admin_cap, &mut s, trusted_pubkey, expiry_time); - let update = parse_and_verify_le_ecdsa_update(hex_message); + let update = parse_and_verify_le_ecdsa_update(&s, &clock, TEST_LAZER_UPDATE); - // If we reach this point, the function worked correctly - // (no assertion failures in parse_and_validate_update) + // If we reach this point, the function successfully verified & parsed the payload (no assertion failures) + // Validate that the fields have correct values assert!(update.timestamp() == 1755625313400000, 0); assert!(update.channel() == new_fixed_rate_200ms(), 0); assert!(vector::length(&update.feeds()) == 3, 0); @@ -63,8 +80,14 @@ public fun test_parse_and_verify_le_ecdsa_update() { let feed_1 = vector::borrow(&update.feeds(), 0); assert!(feed_1.feed_id() == 1, 0); assert!(feed_1.price() == option::some(option::some(i64::from_u64(11350721594969))), 0); - assert!(feed_1.best_bid_price() == option::some(option::some(i64::from_u64(11350696257890))), 0); - assert!(feed_1.best_ask_price() == option::some(option::some(i64::from_u64(11350868428965))), 0); + assert!( + feed_1.best_bid_price() == option::some(option::some(i64::from_u64(11350696257890))), + 0, + ); + assert!( + feed_1.best_ask_price() == option::some(option::some(i64::from_u64(11350868428965))), + 0, + ); assert!(feed_1.exponent() == option::some(i16::new(8, true)), 0); assert!(feed_1.publisher_count() == option::none(), 0); assert!(feed_1.confidence() == option::none(), 0); @@ -95,4 +118,112 @@ public fun test_parse_and_verify_le_ecdsa_update() { assert!(feed_3.funding_rate() == option::some(option::some(i64::from_u64(31670000))), 0); assert!(feed_3.funding_timestamp() == option::some(option::some(1755619200000000)), 0); assert!(feed_3.funding_rate_interval() == option::some(option::some(28800000000)), 0); + + // Clean up + state::destroy_for_test(s); + admin::destroy_for_test(admin_cap); + clock::destroy_for_testing(clock); +} + +#[test] +public fun test_verify_le_ecdsa_message_success() { + let mut ctx = tx_context::dummy(); + let mut s = state::new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); + let clock = clock::create_for_testing(&mut ctx); + + // Add the trusted signer + let expiry_time = 20000000000000000; // Far in the future + state::update_trusted_signer(&admin_cap, &mut s, TEST_TRUSTED_SIGNER_PUBKEY, expiry_time); + + // This should succeed + verify_le_ecdsa_message(&s, &clock, &TEST_SIGNATURE, &TEST_PAYLOAD); + + // Clean up + state::destroy_for_test(s); + admin::destroy_for_test(admin_cap); + clock::destroy_for_testing(clock); +} + +#[test, expected_failure(abort_code = ESignerNotTrusted)] +public fun test_verify_le_ecdsa_message_untrusted_signer() { + let mut ctx = tx_context::dummy(); + let mut s = state::new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); + let clock = clock::create_for_testing(&mut ctx); + + // Don't add any trusted signers - this should fail with ESignerNotTrusted + verify_le_ecdsa_message(&s, &clock, &TEST_SIGNATURE, &TEST_PAYLOAD); + + // Add signers that don't match the signature + let trusted_pubkey1 = x"03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let trusted_pubkey2 = x"03bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + state::update_trusted_signer(&admin_cap, &mut s, trusted_pubkey1, 1000000000000000); + state::update_trusted_signer(&admin_cap, &mut s, trusted_pubkey2, 1000000000000000); + + // This should still fail with ESignerNotTrusted since the signature doesn't match any of the signers + verify_le_ecdsa_message(&s, &clock, &TEST_SIGNATURE, &TEST_PAYLOAD); + + // Clean up + state::destroy_for_test(s); + admin::destroy_for_test(admin_cap); + clock::destroy_for_testing(clock); +} + +#[test, expected_failure(abort_code = ESignerExpired)] +public fun test_verify_le_ecdsa_message_expired_signer() { + let mut ctx = tx_context::dummy(); + let mut s = state::new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); + let mut clock = clock::create_for_testing(&mut ctx); + + let signature = + x"42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101"; + let payload = + x"75d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000"; + + // Add an expired signer + let trusted_pubkey = x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b"; + let expiry_time = 1000000000000000; + clock.set_for_testing(expiry_time); // Advance clock to signer expiry + + state::update_trusted_signer(&admin_cap, &mut s, trusted_pubkey, expiry_time); + + // This should fail with ESignerExpired + verify_le_ecdsa_message(&s, &clock, &signature, &payload); + + // Clean up + state::destroy_for_test(s); + admin::destroy_for_test(admin_cap); + clock::destroy_for_testing(clock); +} + +#[test] +public fun test_verify_le_ecdsa_message_multiple_signers() { + let mut ctx = tx_context::dummy(); + let mut s = state::new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); + let clock = clock::create_for_testing(&mut ctx); + + // Extract signature and payload from the test data + let signature = + x"42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101"; + let payload = + x"75d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000"; + + // Add multiple trusted signers + let trusted_pubkey1 = x"03bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; // This doesn't match our signature + let trusted_pubkey2 = x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b"; // This does + let expiry_time = 1000000000000000; + + state::update_trusted_signer(&admin_cap, &mut s, trusted_pubkey1, expiry_time); + state::update_trusted_signer(&admin_cap, &mut s, trusted_pubkey2, expiry_time); + + // This should succeed because trusted_pubkey2 matches the signature + verify_le_ecdsa_message(&s, &clock, &signature, &payload); + + // Clean up + state::destroy_for_test(s); + admin::destroy_for_test(admin_cap); + clock::destroy_for_testing(clock); }