diff --git a/stellar/stealth-announcer/audits/2026-05-gpt-5-3-codex.md b/stellar/stealth-announcer/audits/2026-05-gpt-5-3-codex.md index c843563..5d02b2d 100644 --- a/stellar/stealth-announcer/audits/2026-05-gpt-5-3-codex.md +++ b/stellar/stealth-announcer/audits/2026-05-gpt-5-3-codex.md @@ -16,7 +16,7 @@ Reviewed files and references: - `contracts/evm/contracts/interfaces/IERC5564Announcer.sol` - Soroban auth model documentation requested in the issue brief -Testing added in `contracts/stellar/stealth-announcer/tests/audit.rs` reproduces each finding. +Testing added in `contracts/stellar/stealth-announcer/tests/audit.rs` originally reproduced each v1 finding. After the v2 redesign, the same audit test file keeps coverage for the historical finding categories while asserting the new v2 event shape and migration guard rails. ## Findings table @@ -81,8 +81,23 @@ This is Informational because permissionless announcement is an intended design **Recommendation:** Keep `announce` permissionless if ERC-5564-style public announcing is the desired model, but do not let clients infer user authorization from the event alone. If caller provenance matters, implement the WA-ANN-01 recommendation with an explicit authenticated caller or explicitly document that Stellar announcements are anonymous/unattributed and CPI-safe by design. +## V2 redesign notes + +The follow-up Stellar announcer implementation intentionally keeps the v1 audit findings above as historical contract documentation instead of rewriting them in place. Existing v1 events used topics `(announce, scheme_id, stealth_address)` and data `(caller, ephemeral_pub_key, metadata)`, so indexers must continue parsing that stream with the v1 schema. The v2 compatibility path is a new announcer deployment, not reinterpretation of already-emitted v1 events. + +The v2 deployment addresses the event-shape ambiguity by fixing `STELLAR_V2_SCHEME_ID = 2` and enforcing it with a strict `assert_eq!(scheme_id, STELLAR_V2_SCHEME_ID)` guard. That makes accidental v1-style calls fail fast and gives indexers an unambiguous scheme filter for the new deployment. + +The v2 event shape is: + +- Topics: `(announce, scheme_id, view_tag_bucket, metadata_kind)` +- Data: `(stealth_address, ephemeral_pub_key, metadata)` + +`view_tag_bucket` is derived stably as `metadata[0] as u32`, and `metadata_kind = 1` records that the first metadata byte is the view tag while the remaining bytes are scheme-specific metadata. This moves the indexed stealth-address-sized topic out of the topic set, replaces it with a compact recipient scan bucket, and removes the misleading v1 `caller` payload field from the v2 data tuple. + +The v2 redesign resolves the WA-ANN-01 caller-attribution ambiguity for new deployments by no longer publishing the announcer contract address as a caller-equivalent payload. WA-ANN-02 and WA-ANN-03 remain operational/client-hardening considerations: v2 still accepts large metadata subject to Soroban resource limits, and `BytesN<32>` still does not prove that an ephemeral public key is curve-valid. WA-ANN-04 remains an intended permissionless-announcement property, but CPI-originated v2 events are no longer coupled to a misleading `caller` value. + ## Additional observations - Dependency pinning is simple: the Stellar workspace pins `soroban-sdk` through a workspace dependency. The audit did not identify risky optional runtime features in the announcer crate; `testutils` is limited to dev-dependencies. -- Topic ordering is deterministic and covered by existing in-tree tests: `(symbol_short!("announce"), scheme_id, stealth_address)`. Keep this order stable because indexers likely key on it. +- Historical v1 topic ordering remains documented as `(symbol_short!("announce"), scheme_id, stealth_address)`. The v2 tests cover the new deterministic ordering `(symbol_short!("announce"), scheme_id, view_tag_bucket, metadata_kind)`, and indexers must not parse one shape as the other. - The crate now declares `rlib` in addition to `cdylib` so integration audit tests can import the generated client. This does not change deployed WASM behavior. diff --git a/stellar/stealth-announcer/src/lib.rs b/stellar/stealth-announcer/src/lib.rs index 78f47c5..074e8dd 100644 --- a/stellar/stealth-announcer/src/lib.rs +++ b/stellar/stealth-announcer/src/lib.rs @@ -2,22 +2,65 @@ use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, BytesN, Env}; +/// Stellar v2 deployment scheme id. +/// +/// The v1 Stellar announcer emitted topics as +/// `(announce, scheme_id, stealth_address)` and data as +/// `(caller, ephemeral_pub_key, metadata)`. That historical shape is not +/// rewritten in place because existing indexers may already rely on it. The v2 +/// rollout is a new announcer deployment that only accepts `scheme_id = 2` and +/// emits the bucketed event shape documented below. +pub const STELLAR_V2_SCHEME_ID: u32 = 2; + +/// Initial metadata kind for v2 announcements. +/// +/// `1` means `metadata[0]` is the one-byte view tag used for pre-filtering and +/// the remaining bytes, if any, are scheme-specific metadata. Future metadata +/// encodings must reserve a new `metadata_kind` value instead of changing this +/// interpretation. +pub const METADATA_KIND_VIEW_TAG: u32 = 1; + +/// Derives the indexed view-tag bucket for v2 announcement topics. +/// +/// The bucket is exactly the first metadata byte interpreted as an unsigned +/// integer in `[0, 255]`. Because `METADATA_KIND_VIEW_TAG` commits to the first +/// byte being present, callers must provide non-empty metadata. +pub fn view_tag_bucket(metadata: &Bytes) -> u32 { + metadata.get(0).expect("metadata must include view tag") as u32 +} + #[contract] pub struct StealthAnnouncerContract; #[contractimpl] impl StealthAnnouncerContract { - /// Emits a stealth address announcement event. + /// Emits a Stellar v2 stealth address announcement event. /// /// This is a pure event-emission function with no access control and no /// storage. Indexers watch for these events to let recipients detect /// incoming payments. /// + /// v2 event shape: + /// * topics: `("announce", scheme_id, view_tag_bucket, metadata_kind)` + /// * data: `(stealth_address, ephemeral_pub_key, metadata)` + /// + /// The stable `view_tag_bucket` derivation is `metadata[0] as u32`, where + /// `metadata_kind = 1` (`METADATA_KIND_VIEW_TAG`) means the first metadata + /// byte is the view tag and the remaining bytes are scheme-specific. This + /// lets wallets and indexers filter Stellar RPC `getEvents` by scheme and + /// bucket before doing client-side cryptographic validation. + /// + /// Migration note: v1 announcements used the old Stellar layout + /// `("announce", scheme_id, stealth_address)` with + /// `(caller, ephemeral_pub_key, metadata)`. Do not reinterpret historical v1 + /// events as v2. The compatibility path is a new announcer deployment using + /// `scheme_id = 2`. + /// /// # Arguments - /// * `scheme_id` - Identifier for the stealth address scheme (e.g. 1 for the default DKSAP scheme). + /// * `scheme_id` - Must be `2` for the v2 Stellar announcer deployment. /// * `stealth_address` - The one-time stealth address that received funds. /// * `ephemeral_pub_key` - The ephemeral public key used to derive the stealth address. - /// * `metadata` - Arbitrary metadata (e.g. view tag) to speed up scanning. + /// * `metadata` - Non-empty metadata whose first byte is the view tag. pub fn announce( env: Env, scheme_id: u32, @@ -25,9 +68,19 @@ impl StealthAnnouncerContract { ephemeral_pub_key: BytesN<32>, metadata: Bytes, ) { + assert_eq!(scheme_id, STELLAR_V2_SCHEME_ID); + + let view_tag_bucket = view_tag_bucket(&metadata); + let metadata_kind = METADATA_KIND_VIEW_TAG; + env.events().publish( - (symbol_short!("announce"), scheme_id, stealth_address), - (env.current_contract_address(), ephemeral_pub_key, metadata), + ( + symbol_short!("announce"), + scheme_id, + view_tag_bucket, + metadata_kind, + ), + (stealth_address, ephemeral_pub_key, metadata), ); } } @@ -35,8 +88,8 @@ impl StealthAnnouncerContract { #[cfg(test)] mod test { use super::*; - use soroban_sdk::testutils::{Address as _, Events}; - use soroban_sdk::{vec, Address, Bytes, BytesN, Env, IntoVal, Val}; + use soroban_sdk::testutils::{Address as _, EnvTestConfig, Events}; + use soroban_sdk::{vec, Address, Bytes, BytesN, Env, FromVal, IntoVal, Val}; #[test] fn test_announce_emits_event() { @@ -46,8 +99,8 @@ mod test { let stealth_address = Address::generate(&env); let ephemeral_pub_key = BytesN::from_array(&env, &[1u8; 32]); - let metadata = Bytes::from_slice(&env, &[0u8; 1]); - let scheme_id: u32 = 1; + let metadata = Bytes::from_slice(&env, &[42u8, 7u8]); + let scheme_id: u32 = STELLAR_V2_SCHEME_ID; client.announce(&scheme_id, &stealth_address, &ephemeral_pub_key, &metadata); @@ -59,51 +112,88 @@ mod test { // Verify the event was published by the correct contract. assert_eq!(event.0, contract_id); - // Verify topics: ("announce", scheme_id, stealth_address). + // Verify topics: ("announce", scheme_id, view_tag_bucket, metadata_kind). let expected_topics: soroban_sdk::Vec = vec![ &env, symbol_short!("announce").into_val(&env), scheme_id.into_val(&env), - stealth_address.into_val(&env), + 42u32.into_val(&env), + METADATA_KIND_VIEW_TAG.into_val(&env), ]; assert_eq!(event.1, expected_topics); + + // Verify data: (stealth_address, ephemeral_pub_key, metadata). + let actual_value: (Address, BytesN<32>, Bytes) = FromVal::from_val(&env, &event.2); + assert_eq!(actual_value, (stealth_address, ephemeral_pub_key, metadata)); } #[test] - fn test_announce_different_schemes() { + fn test_view_tag_bucket_derives_from_first_metadata_byte() { let env = Env::default(); let contract_id = env.register(StealthAnnouncerContract, ()); let client = StealthAnnouncerContractClient::new(&env, &contract_id); let addr = Address::generate(&env); let epk = BytesN::from_array(&env, &[1u8; 32]); - let meta = Bytes::from_slice(&env, &[0u8; 1]); + let first_meta = Bytes::from_slice(&env, &[0u8, 99u8]); + let second_meta = Bytes::from_slice(&env, &[255u8, 99u8]); - // Announce with scheme_id = 1. - client.announce(&1u32, &addr, &epk, &meta); + client.announce(&STELLAR_V2_SCHEME_ID, &addr, &epk, &first_meta); let events = env.events().all(); - assert!(!events.is_empty()); let event = events.last().unwrap(); assert_eq!(event.0, contract_id.clone()); let expected_topics: soroban_sdk::Vec = vec![ &env, symbol_short!("announce").into_val(&env), - 1u32.into_val(&env), - addr.clone().into_val(&env), + STELLAR_V2_SCHEME_ID.into_val(&env), + 0u32.into_val(&env), + METADATA_KIND_VIEW_TAG.into_val(&env), ]; assert_eq!(event.1, expected_topics); - // Announce again with scheme_id = 2 — still works. - client.announce(&2u32, &addr, &epk, &meta); + client.announce(&STELLAR_V2_SCHEME_ID, &addr, &epk, &second_meta); let events2 = env.events().all(); let event2 = events2.last().unwrap(); let expected_topics2: soroban_sdk::Vec = vec![ &env, symbol_short!("announce").into_val(&env), - 2u32.into_val(&env), - addr.into_val(&env), + STELLAR_V2_SCHEME_ID.into_val(&env), + 255u32.into_val(&env), + METADATA_KIND_VIEW_TAG.into_val(&env), ]; assert_eq!(event2.1, expected_topics2); } + + #[test] + #[should_panic] + fn test_announce_rejects_v1_scheme_id() { + let env = Env::new_with_config(EnvTestConfig { + capture_snapshot_at_drop: false, + }); + let contract_id = env.register(StealthAnnouncerContract, ()); + let client = StealthAnnouncerContractClient::new(&env, &contract_id); + + let addr = Address::generate(&env); + let epk = BytesN::from_array(&env, &[1u8; 32]); + let meta = Bytes::from_slice(&env, &[0u8; 1]); + + client.announce(&1u32, &addr, &epk, &meta); + } + + #[test] + #[should_panic] + fn test_announce_rejects_missing_view_tag() { + let env = Env::new_with_config(EnvTestConfig { + capture_snapshot_at_drop: false, + }); + let contract_id = env.register(StealthAnnouncerContract, ()); + let client = StealthAnnouncerContractClient::new(&env, &contract_id); + + let addr = Address::generate(&env); + let epk = BytesN::from_array(&env, &[1u8; 32]); + let meta = Bytes::new(&env); + + client.announce(&STELLAR_V2_SCHEME_ID, &addr, &epk, &meta); + } } diff --git a/stellar/stealth-announcer/test_snapshots/test/test_announce_emits_event.1.json b/stellar/stealth-announcer/test_snapshots/test/test_announce_emits_event.1.json index f80a4d2..a5d9aa7 100644 --- a/stellar/stealth-announcer/test_snapshots/test/test_announce_emits_event.1.json +++ b/stellar/stealth-announcer/test_snapshots/test/test_announce_emits_event.1.json @@ -85,22 +85,25 @@ "symbol": "announce" }, { - "u32": 1 + "u32": 2 + }, + { + "u32": 42 }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + "u32": 1 } ], "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" }, { "bytes": "0101010101010101010101010101010101010101010101010101010101010101" }, { - "bytes": "00" + "bytes": "2a07" } ] } diff --git a/stellar/stealth-announcer/test_snapshots/test/test_announce_different_schemes.1.json b/stellar/stealth-announcer/test_snapshots/test/test_view_tag_bucket_derives_from_first_metadata_byte.1.json similarity index 95% rename from stellar/stealth-announcer/test_snapshots/test/test_announce_different_schemes.1.json rename to stellar/stealth-announcer/test_snapshots/test/test_view_tag_bucket_derives_from_first_metadata_byte.1.json index b9acfab..bf857fb 100644 --- a/stellar/stealth-announcer/test_snapshots/test/test_announce_different_schemes.1.json +++ b/stellar/stealth-announcer/test_snapshots/test/test_view_tag_bucket_derives_from_first_metadata_byte.1.json @@ -89,19 +89,22 @@ "u32": 2 }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + "u32": 255 + }, + { + "u32": 1 } ], "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" }, { "bytes": "0101010101010101010101010101010101010101010101010101010101010101" }, { - "bytes": "00" + "bytes": "ff63" } ] } diff --git a/stellar/stealth-announcer/tests/audit.rs b/stellar/stealth-announcer/tests/audit.rs index 734a347..e7a269f 100644 --- a/stellar/stealth-announcer/tests/audit.rs +++ b/stellar/stealth-announcer/tests/audit.rs @@ -2,7 +2,10 @@ use soroban_sdk::testutils::{Address as _, EnvTestConfig, Events}; use soroban_sdk::{ contract, contractimpl, symbol_short, vec, Address, Bytes, BytesN, Env, FromVal, IntoVal, Val, }; -use stealth_announcer::{StealthAnnouncerContract, StealthAnnouncerContractClient}; +use stealth_announcer::{ + StealthAnnouncerContract, StealthAnnouncerContractClient, METADATA_KIND_VIEW_TAG, + STELLAR_V2_SCHEME_ID, +}; fn audit_env() -> Env { Env::new_with_config(EnvTestConfig { @@ -11,7 +14,7 @@ fn audit_env() -> Env { } #[test] -fn wa_ann_01_caller_payload_is_contract_not_invoker() { +fn wa_ann_01_v2_payload_uses_stealth_address_not_caller() { let env = audit_env(); let contract_id = env.register(StealthAnnouncerContract, ()); let client = StealthAnnouncerContractClient::new(&env, &contract_id); @@ -21,14 +24,19 @@ fn wa_ann_01_caller_payload_is_contract_not_invoker() { let ephemeral_pub_key = BytesN::from_array(&env, &[1u8; 32]); let metadata = Bytes::from_slice(&env, &[0u8; 1]); - client.announce(&1u32, &stealth_address, &ephemeral_pub_key, &metadata); + client.announce( + &STELLAR_V2_SCHEME_ID, + &stealth_address, + &ephemeral_pub_key, + &metadata, + ); let events = env.events().all(); let event = events.last().unwrap(); let actual_value: (Address, BytesN<32>, Bytes) = FromVal::from_val(&env, &event.2); assert_ne!(contract_id, invoker); - assert_eq!(actual_value, (contract_id, ephemeral_pub_key, metadata)); + assert_eq!(actual_value, (stealth_address, ephemeral_pub_key, metadata)); } #[test] @@ -41,13 +49,18 @@ fn wa_ann_02_oversized_metadata_is_accepted() { let ephemeral_pub_key = BytesN::from_array(&env, &[2u8; 32]); let metadata = Bytes::from_array(&env, &[7u8; 4096]); - client.announce(&1u32, &stealth_address, &ephemeral_pub_key, &metadata); + client.announce( + &STELLAR_V2_SCHEME_ID, + &stealth_address, + &ephemeral_pub_key, + &metadata, + ); let events = env.events().all(); let event = events.last().unwrap(); let actual_value: (Address, BytesN<32>, Bytes) = FromVal::from_val(&env, &event.2); - assert_eq!(actual_value, (contract_id, ephemeral_pub_key, metadata)); + assert_eq!(actual_value, (stealth_address, ephemeral_pub_key, metadata)); } #[test] @@ -60,7 +73,12 @@ fn wa_ann_03_zero_ephemeral_pub_key_is_accepted() { let zero_ephemeral_pub_key = BytesN::from_array(&env, &[0u8; 32]); let metadata = Bytes::from_slice(&env, &[0u8; 1]); - client.announce(&1u32, &stealth_address, &zero_ephemeral_pub_key, &metadata); + client.announce( + &STELLAR_V2_SCHEME_ID, + &stealth_address, + &zero_ephemeral_pub_key, + &metadata, + ); let events = env.events().all(); let event = events.last().unwrap(); @@ -68,7 +86,7 @@ fn wa_ann_03_zero_ephemeral_pub_key_is_accepted() { assert_eq!( actual_value, - (contract_id, zero_ephemeral_pub_key, metadata) + (stealth_address, zero_ephemeral_pub_key, metadata) ); } @@ -103,7 +121,7 @@ fn wa_ann_04_cpi_can_emit_announcements_without_auth() { forwarder.forward( &announcer_id, - &1u32, + &STELLAR_V2_SCHEME_ID, &stealth_address, &ephemeral_pub_key, &metadata, @@ -114,12 +132,13 @@ fn wa_ann_04_cpi_can_emit_announcements_without_auth() { let expected_topics: soroban_sdk::Vec = vec![ &env, symbol_short!("announce").into_val(&env), - 1u32.into_val(&env), - stealth_address.into_val(&env), + STELLAR_V2_SCHEME_ID.into_val(&env), + 0u32.into_val(&env), + METADATA_KIND_VIEW_TAG.into_val(&env), ]; let actual_value: (Address, BytesN<32>, Bytes) = FromVal::from_val(&env, &event.2); assert_eq!(event.0, announcer_id.clone()); assert_eq!(event.1, expected_topics); - assert_eq!(actual_value, (announcer_id, ephemeral_pub_key, metadata)); + assert_eq!(actual_value, (stealth_address, ephemeral_pub_key, metadata)); } diff --git a/stellar/stealth-announcer/tests/properties.rs b/stellar/stealth-announcer/tests/properties.rs index 5fc9d6e..7821446 100644 --- a/stellar/stealth-announcer/tests/properties.rs +++ b/stellar/stealth-announcer/tests/properties.rs @@ -1,7 +1,10 @@ use proptest::prelude::*; use soroban_sdk::testutils::{Address as _, EnvTestConfig, Events}; use soroban_sdk::{symbol_short, vec, Address, Bytes, BytesN, Env, IntoVal, TryFromVal, Val}; -use stealth_announcer::{StealthAnnouncerContract, StealthAnnouncerContractClient}; +use stealth_announcer::{ + view_tag_bucket, StealthAnnouncerContract, StealthAnnouncerContractClient, + METADATA_KIND_VIEW_TAG, STELLAR_V2_SCHEME_ID, +}; fn cases() -> u32 { std::env::var("WRAITH_PROPTEST_CASES") @@ -29,38 +32,41 @@ fn bytes32(env: &Env, data: &[u8]) -> BytesN<32> { proptest! { #![proptest_config(ProptestConfig { cases: cases(), .. ProptestConfig::default() })] #[test] - fn announces_once_for_valid_payloads(scheme_id in any::(), epk in any::<[u8; 32]>(), metadata in prop::collection::vec(any::(), 0..128)) { + fn announces_once_for_valid_v2_payloads(epk in any::<[u8; 32]>(), metadata in prop::collection::vec(any::(), 1..128)) { let env = env(); let contract_id = env.register(StealthAnnouncerContract, ()); let client = StealthAnnouncerContractClient::new(&env, &contract_id); let stealth_address = Address::generate(&env); - client.announce(&scheme_id, &stealth_address, &bytes32(&env, &epk), &bytes(&env, &metadata)); + client.announce(&STELLAR_V2_SCHEME_ID, &stealth_address, &bytes32(&env, &epk), &bytes(&env, &metadata)); prop_assert_eq!(env.events().all().len(), 1); } #[test] - fn topics_round_trip_verbatim(scheme_id in any::(), epk in any::<[u8; 32]>(), metadata in prop::collection::vec(any::(), 0..128)) { + fn v2_topics_include_scheme_view_tag_bucket_and_metadata_kind(epk in any::<[u8; 32]>(), metadata in prop::collection::vec(any::(), 1..128)) { let env = env(); let contract_id = env.register(StealthAnnouncerContract, ()); let client = StealthAnnouncerContractClient::new(&env, &contract_id); let stealth_address = Address::generate(&env); + let metadata = bytes(&env, &metadata); + let bucket = view_tag_bucket(&metadata); - client.announce(&scheme_id, &stealth_address, &bytes32(&env, &epk), &bytes(&env, &metadata)); + client.announce(&STELLAR_V2_SCHEME_ID, &stealth_address, &bytes32(&env, &epk), &metadata); let event = env.events().all().last().unwrap(); let expected_topics: soroban_sdk::Vec = vec![ &env, symbol_short!("announce").into_val(&env), - scheme_id.into_val(&env), - stealth_address.into_val(&env), + STELLAR_V2_SCHEME_ID.into_val(&env), + bucket.into_val(&env), + METADATA_KIND_VIEW_TAG.into_val(&env), ]; prop_assert_eq!(event.1, expected_topics); } #[test] - fn payload_round_trips_verbatim(scheme_id in any::(), epk in any::<[u8; 32]>(), metadata in prop::collection::vec(any::(), 0..128)) { + fn v2_payload_round_trips_without_caller(epk in any::<[u8; 32]>(), metadata in prop::collection::vec(any::(), 1..128)) { let env = env(); let contract_id = env.register(StealthAnnouncerContract, ()); let client = StealthAnnouncerContractClient::new(&env, &contract_id); @@ -68,47 +74,70 @@ proptest! { let epk = bytes32(&env, &epk); let metadata = bytes(&env, &metadata); - client.announce(&scheme_id, &stealth_address, &epk, &metadata); + client.announce(&STELLAR_V2_SCHEME_ID, &stealth_address, &epk, &metadata); let event = env.events().all().last().unwrap(); let actual_value: (Address, BytesN<32>, Bytes) = <(Address, BytesN<32>, Bytes)>::try_from_val(&env, &event.2).unwrap(); - prop_assert_eq!(actual_value, (contract_id, epk, metadata)); + prop_assert_eq!(actual_value, (stealth_address, epk, metadata)); } #[test] - fn repeated_announcements_publish_latest_call(scheme_id in any::(), next_scheme_id in any::(), epk in any::<[u8; 32]>()) { + fn repeated_v2_announcements_publish_latest_view_tag_bucket(epk in any::<[u8; 32]>(), first_view_tag in any::(), second_view_tag in any::()) { let env = env(); let contract_id = env.register(StealthAnnouncerContract, ()); let client = StealthAnnouncerContractClient::new(&env, &contract_id); let stealth_address = Address::generate(&env); let epk = bytes32(&env, &epk); - let metadata = bytes(&env, &[7]); + let first_metadata = bytes(&env, &[first_view_tag]); + let second_metadata = bytes(&env, &[second_view_tag]); - client.announce(&scheme_id, &stealth_address, &epk, &metadata); - client.announce(&next_scheme_id, &stealth_address, &epk, &metadata); + client.announce(&STELLAR_V2_SCHEME_ID, &stealth_address, &epk, &first_metadata); + client.announce(&STELLAR_V2_SCHEME_ID, &stealth_address, &epk, &second_metadata); let event = env.events().all().last().unwrap(); let expected_topics: soroban_sdk::Vec = vec![ &env, symbol_short!("announce").into_val(&env), - next_scheme_id.into_val(&env), - stealth_address.into_val(&env), + STELLAR_V2_SCHEME_ID.into_val(&env), + (second_view_tag as u32).into_val(&env), + METADATA_KIND_VIEW_TAG.into_val(&env), ]; prop_assert_eq!(event.1, expected_topics); } +} - #[test] - fn zero_length_metadata_is_valid(scheme_id in any::(), epk in any::<[u8; 32]>()) { - let env = env(); - let contract_id = env.register(StealthAnnouncerContract, ()); - let client = StealthAnnouncerContractClient::new(&env, &contract_id); - let stealth_address = Address::generate(&env); - - client.announce(&scheme_id, &stealth_address, &bytes32(&env, &epk), &Bytes::new(&env)); +#[test] +#[should_panic] +fn zero_length_metadata_is_rejected() { + let env = env(); + let contract_id = env.register(StealthAnnouncerContract, ()); + let client = StealthAnnouncerContractClient::new(&env, &contract_id); + let stealth_address = Address::generate(&env); + + client.announce( + &STELLAR_V2_SCHEME_ID, + &stealth_address, + &bytes32(&env, &[1u8; 32]), + &Bytes::new(&env), + ); +} - prop_assert_eq!(env.events().all().len(), 1); - } +#[test] +#[should_panic] +fn non_v2_scheme_id_is_rejected() { + let env = env(); + let contract_id = env.register(StealthAnnouncerContract, ()); + let client = StealthAnnouncerContractClient::new(&env, &contract_id); + let stealth_address = Address::generate(&env); + let metadata = bytes(&env, &[7]); + + client.announce( + &1u32, + &stealth_address, + &bytes32(&env, &[1u8; 32]), + &metadata, + ); } #[test]