diff --git a/stellar/PERF.md b/stellar/PERF.md index d764f07..520a7b1 100644 --- a/stellar/PERF.md +++ b/stellar/PERF.md @@ -42,6 +42,10 @@ These baseline numbers were captured before the optimization below. | stealth-sender | batch_send | batch_size=5 | 807519 | 120229 | 5 | 7 | 1068 | 1416 | 2420 | | stealth-sender | batch_send | batch_size=10 | 1633634 | 245649 | 5 | 12 | 1068 | 2536 | 4840 | | stealth-sender | batch_send | batch_size=25 | 4322337 | 690609 | 5 | 27 | 1068 | 5896 | 12100 | +| stealth-sender | sponsored_announce | batch_size=1 | 233608 | 36052 | 7 | 4 | 1164 | 592 | 916 | +| stealth-sender | sponsored_announce | batch_size=5 | 1098506 | 193054 | 11 | 16 | 2060 | 2672 | 2852 | +| stealth-sender | sponsored_announce | batch_size=10 | 2445729 | 491659 | 16 | 31 | 3180 | 5272 | 5272 | +| stealth-sender | sponsored_announce | batch_size=20 | 6018914 | 1430044 | 26 | 61 | 5420 | 10472 | 10112 | | wraith-names | register | name_len=3 | 59800 | 6269 | 1 | 2 | 104 | 544 | 204 | | wraith-names | register | name_len=32 | 61413 | 6327 | 1 | 2 | 104 | 572 | 232 | | wraith-names | resolve | hit | 46120 | 5537 | 1 | 0 | 476 | 0 | 0 | @@ -176,6 +180,18 @@ Mermaid source: [`bench/data/crossover-chart.md`](bench/data/crossover-chart.md) Expected savings: low to medium; correctness and readability should be preserved. +## Sponsored Announcement Cost + +The sponsored rows were measured with distinct authenticated senders per +entry. Average resources per entry are calculated from each row: + +| Batch size | Instructions/entry | Mem bytes/entry | Event bytes/entry | +|---:|---:|---:|---:| +| 1 | 233608 | 36052 | 916 | +| 5 | 219701 | 38611 | 570 | +| 10 | 244573 | 49166 | 527 | +| 20 | 300946 | 71502 | 506 | + ## Concrete Diff Suggestions 1. Batch send: add a dedicated announcer batch API and invoke it once after all diff --git a/stellar/bench/src/main.rs b/stellar/bench/src/main.rs index 7f4fb2a..0f6e2c0 100644 --- a/stellar/bench/src/main.rs +++ b/stellar/bench/src/main.rs @@ -120,6 +120,36 @@ fn main() { )); } + for batch_size in [1u32, 5, 10, 20] { + rows.push(measure( + "stealth-sender", + "sponsored_announce", + format!("batch_size={batch_size}"), + |env| { + env.mock_all_auths(); + let sender_contract_id = env.register(StealthSenderContract, ()); + let announcer_id = env.register(StealthAnnouncerContract, ()); + let client = StealthSenderContractClient::new(env, &sender_contract_id); + client.init(&announcer_id, &None, &None, &0); + let (token, sender) = funded_token(env, true); + let sponsor = Address::generate(env); + let mut entries: SorobanVec = vec![env]; + for i in 0..batch_size { + entries.push_back(stealth_sender::SponsoredEntry { + sender: sender.clone(), + token: token.clone(), + amount: 100, + scheme_id: 1, + stealth_address: Address::generate(env), + ephemeral_pub_key: BytesN::from_array(env, &[i as u8; 32]), + metadata: bytes(env, 32, i as u8), + }); + } + client.sponsored_announce(&sponsor, &entries); + }, + )); + } + for name_len in [3u32, 32] { rows.push(measure( "wraith-names", diff --git a/stellar/bindings/typescript/stealth-sender/src/index.ts b/stellar/bindings/typescript/stealth-sender/src/index.ts index 6d59b49..b967aa3 100644 --- a/stellar/bindings/typescript/stealth-sender/src/index.ts +++ b/stellar/bindings/typescript/stealth-sender/src/index.ts @@ -36,7 +36,7 @@ if (typeof window !== "undefined") { /** * Storage keys. */ -export type DataKey = {tag: "Announcer", values: void}; +export type DataKey = {tag: "Announcer", values: void} | {tag: "AssetPolicy", values: void} | {tag: "FeeRecipient", values: void} | {tag: "FeeBasisPoints", values: void}; /** * Errors that the sender contract can produce. @@ -53,17 +53,70 @@ export const SenderError = { /** * The batch input vectors have mismatched lengths. */ - 3: {message:"LengthMismatch"} + 3: {message:"LengthMismatch"}, + /** + * The token is not allowed by the asset policy. + */ + 4: {message:"TokenNotAllowed"}, + /** + * The fee configuration is invalid (e.g. fee > 50 bps, or fee > 0 with no recipient). + */ + 5: {message:"InvalidFeeConfig"}, + /** + * The sponsored announcement batch exceeds the maximum entry count. + */ + 6: {message:"SponsoredBatchTooLarge"} +} + + +/** + * A token transfer and announcement authenticated by its sender. + */ +export interface SponsoredEntry { + amount: i128; + ephemeral_pub_key: Buffer; + metadata: Buffer; + scheme_id: u32; + sender: string; + stealth_address: string; + token: string; +} + + +/** + * Wraith Protocol standard metric event schema. + * + * All Wraith contracts emit metric events using this structure to enable + * standardized off-chain observability and monitoring. + */ +export interface WraithMetricEvent { + /** + * Contract identifier (e.g., "stealth-registry", "stealth-sender") + */ +contract: string; + /** + * Optional dimensions for filtering/grouping (e.g., token_address, scheme_id) + */ +dimensions: Array; + /** + * Metric name (e.g., "register_count", "send_volume") + */ +metric_name: string; + /** + * Numeric value of the metric + */ +value: i128; } export interface Client { /** * Construct and simulate a init transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - * Initialise the contract by storing the announcer address. + * Initialise the contract by storing the announcer address, optional asset policy, + * and optional protocol fee configuration. * * Must be called exactly once before any `send` or `batch_send`. */ - init: ({announcer}: {announcer: string}, options?: MethodOptions) => Promise>> + init: ({announcer, asset_policy, fee_recipient, fee_basis_points}: {announcer: string, asset_policy: Option, fee_recipient: Option, fee_basis_points: u32}, options?: MethodOptions) => Promise>> /** * Construct and simulate a send transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -89,6 +142,16 @@ export interface Client { */ batch_send: ({sender, token, scheme_id, stealth_addresses, ephemeral_pub_keys, metadatas, amounts}: {sender: string, token: string, scheme_id: u32, stealth_addresses: Array, ephemeral_pub_keys: Array, metadatas: Array, amounts: Array}, options?: MethodOptions) => Promise>> + /** + * Construct and simulate a sponsored_announce transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Transfer tokens and emit announcements for entries paid for by a sponsor. + * + * The sponsor and every entry sender must authorise the invocation. The + * sponsor pays the transaction fee through Stellar fee-bump mechanics; + * entry senders remain responsible for their own token transfers. + */ + sponsored_announce: ({sponsor, entries}: {sponsor: string, entries: Array}, options?: MethodOptions) => Promise>> + } export class Client extends ContractClient { static async deploy( @@ -107,17 +170,21 @@ export class Client extends ContractClient { } constructor(public readonly options: ContractClientOptions) { super( - new ContractSpec([ "AAAAAAAAAHlJbml0aWFsaXNlIHRoZSBjb250cmFjdCBieSBzdG9yaW5nIHRoZSBhbm5vdW5jZXIgYWRkcmVzcy4KCk11c3QgYmUgY2FsbGVkIGV4YWN0bHkgb25jZSBiZWZvcmUgYW55IGBzZW5kYCBvciBgYmF0Y2hfc2VuZGAuAAAAAAAABGluaXQAAAABAAAAAAAAAAlhbm5vdW5jZXIAAAAAAAATAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAALU2VuZGVyRXJyb3IA", + new ContractSpec([ "AAAAAgAAAA1TdG9yYWdlIGtleXMuAAAAAAAAAAAAAAdEYXRhS2V5AAAAAAQAAAAAAAAANlRoZSBhZGRyZXNzIG9mIHRoZSBkZXBsb3llZCBTdGVhbHRoQW5ub3VuY2VyIGNvbnRyYWN0LgAAAAAACUFubm91bmNlcgAAAAAAAAAAAAAuT3B0aW9uYWwgYWRkcmVzcyBvZiB0aGUgYXNzZXQgcG9saWN5IGNvbnRyYWN0LgAAAAAAC0Fzc2V0UG9saWN5AAAAAAAAAAAvT3B0aW9uYWwgYWRkcmVzcyBvZiB0aGUgcHJvdG9jb2wgZmVlIHJlY2lwaWVudC4AAAAADEZlZVJlY2lwaWVudAAAAAAAAAA4UHJvdG9jb2wgZmVlIGluIGJhc2lzIHBvaW50cyAobWF4IDUwIGJwcywgMCA9IGRpc2FibGVkKS4AAAAORmVlQmFzaXNQb2ludHMAAA==", + "AAAABAAAACxFcnJvcnMgdGhhdCB0aGUgc2VuZGVyIGNvbnRyYWN0IGNhbiBwcm9kdWNlLgAAAAAAAAALU2VuZGVyRXJyb3IAAAAABgAAACpUaGUgY29udHJhY3QgaGFzIGFscmVhZHkgYmVlbiBpbml0aWFsaXNlZC4AAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAqVGhlIGNvbnRyYWN0IGhhcyBub3QgYmVlbiBpbml0aWFsaXNlZCB5ZXQuAAAAAAAOTm90SW5pdGlhbGl6ZWQAAAAAAAIAAAAwVGhlIGJhdGNoIGlucHV0IHZlY3RvcnMgaGF2ZSBtaXNtYXRjaGVkIGxlbmd0aHMuAAAADkxlbmd0aE1pc21hdGNoAAAAAAADAAAALVRoZSB0b2tlbiBpcyBub3QgYWxsb3dlZCBieSB0aGUgYXNzZXQgcG9saWN5LgAAAAAAAA9Ub2tlbk5vdEFsbG93ZWQAAAAABAAAAFNUaGUgZmVlIGNvbmZpZ3VyYXRpb24gaXMgaW52YWxpZCAoZS5nLiBmZWUgPiA1MCBicHMsIG9yIGZlZSA+IDAgd2l0aCBubyByZWNpcGllbnQpLgAAAAAQSW52YWxpZEZlZUNvbmZpZwAAAAUAAABBVGhlIHNwb25zb3JlZCBhbm5vdW5jZW1lbnQgYmF0Y2ggZXhjZWVkcyB0aGUgbWF4aW11bSBlbnRyeSBjb3VudC4AAAAAAAAWU3BvbnNvcmVkQmF0Y2hUb29MYXJnZQAAAAAABg==", + "AAAAAQAAAD5BIHRva2VuIHRyYW5zZmVyIGFuZCBhbm5vdW5jZW1lbnQgYXV0aGVudGljYXRlZCBieSBpdHMgc2VuZGVyLgAAAAAAAAAAAA5TcG9uc29yZWRFbnRyeQAAAAAABwAAAAAAAAAGYW1vdW50AAAAAAALAAAAAAAAABFlcGhlbWVyYWxfcHViX2tleQAAAAAAA+4AAAAgAAAAAAAAAAhtZXRhZGF0YQAAAA4AAAAAAAAACXNjaGVtZV9pZAAAAAAAAAQAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAAPc3RlYWx0aF9hZGRyZXNzAAAAABMAAAAAAAAABXRva2VuAAAAAAAAEw==", + "AAAAAAAAALlJbml0aWFsaXNlIHRoZSBjb250cmFjdCBieSBzdG9yaW5nIHRoZSBhbm5vdW5jZXIgYWRkcmVzcywgb3B0aW9uYWwgYXNzZXQgcG9saWN5LAphbmQgb3B0aW9uYWwgcHJvdG9jb2wgZmVlIGNvbmZpZ3VyYXRpb24uCgpNdXN0IGJlIGNhbGxlZCBleGFjdGx5IG9uY2UgYmVmb3JlIGFueSBgc2VuZGAgb3IgYGJhdGNoX3NlbmRgLgAAAAAAAARpbml0AAAABAAAAAAAAAAJYW5ub3VuY2VyAAAAAAAAEwAAAAAAAAAMYXNzZXRfcG9saWN5AAAD6AAAABMAAAAAAAAADWZlZV9yZWNpcGllbnQAAAAAAAPoAAAAEwAAAAAAAAAQZmVlX2Jhc2lzX3BvaW50cwAAAAQAAAABAAAD6QAAA+0AAAAAAAAH0AAAAAtTZW5kZXJFcnJvcgA=", "AAAAAAAAAglUcmFuc2ZlciB0b2tlbnMgdG8gYSBzdGVhbHRoIGFkZHJlc3MgYW5kIGVtaXQgYW4gYW5ub3VuY2VtZW50LgoKIyBBcmd1bWVudHMKKiBgc2VuZGVyYCAgICAgICAgICAgIC0gVGhlIGFkZHJlc3Mgc2VuZGluZyBmdW5kcyAobXVzdCBhdXRob3Jpc2UpLgoqIGB0b2tlbmAgICAgICAgICAgICAgLSBTQUMgdG9rZW4gY29udHJhY3QgYWRkcmVzcyAod29ya3MgZm9yIG5hdGl2ZSBYTE0gdG9vKS4KKiBgYW1vdW50YCAgICAgICAgICAgIC0gQW1vdW50IG9mIHRva2VucyB0byB0cmFuc2Zlci4KKiBgc2NoZW1lX2lkYCAgICAgICAgIC0gU3RlYWx0aCBhZGRyZXNzIHNjaGVtZSBpZGVudGlmaWVyLgoqIGBzdGVhbHRoX2FkZHJlc3NgICAgLSBUaGUgZGVyaXZlZCBvbmUtdGltZSBzdGVhbHRoIGFkZHJlc3MuCiogYGVwaGVtZXJhbF9wdWJfa2V5YCAtIEVwaGVtZXJhbCBwdWJsaWMga2V5IGZvciB0aGUgcmVjaXBpZW50IHRvIHNjYW4uCiogYG1ldGFkYXRhYCAgICAgICAgICAtIEV4dHJhIGRhdGEgKGUuZy4gdmlldyB0YWcpLgAAAAAAAARzZW5kAAAABwAAAAAAAAAGc2VuZGVyAAAAAAATAAAAAAAAAAV0b2tlbgAAAAAAABMAAAAAAAAABmFtb3VudAAAAAAACwAAAAAAAAAJc2NoZW1lX2lkAAAAAAAABAAAAAAAAAAPc3RlYWx0aF9hZGRyZXNzAAAAABMAAAAAAAAAEWVwaGVtZXJhbF9wdWJfa2V5AAAAAAAD7gAAACAAAAAAAAAACG1ldGFkYXRhAAAADgAAAAEAAAPpAAAD7QAAAAAAAAfQAAAAC1NlbmRlckVycm9yAA==", - "AAAAAgAAAA1TdG9yYWdlIGtleXMuAAAAAAAAAAAAAAdEYXRhS2V5AAAAAAEAAAAAAAAANlRoZSBhZGRyZXNzIG9mIHRoZSBkZXBsb3llZCBTdGVhbHRoQW5ub3VuY2VyIGNvbnRyYWN0LgAAAAAACUFubm91bmNlcgAAAA==", "AAAAAAAAAJxCYXRjaCB2ZXJzaW9uIG9mIGBzZW5kYCDigJQgdHJhbnNmZXJzIHRva2VucyB0byBtdWx0aXBsZSBzdGVhbHRoIGFkZHJlc3NlcwphbmQgZW1pdHMgYW4gYW5ub3VuY2VtZW50IGZvciBlYWNoLgoKQWxsIGlucHV0IHZlY3RvcnMgbXVzdCBoYXZlIHRoZSBzYW1lIGxlbmd0aC4AAAAKYmF0Y2hfc2VuZAAAAAAABwAAAAAAAAAGc2VuZGVyAAAAAAATAAAAAAAAAAV0b2tlbgAAAAAAABMAAAAAAAAACXNjaGVtZV9pZAAAAAAAAAQAAAAAAAAAEXN0ZWFsdGhfYWRkcmVzc2VzAAAAAAAD6gAAABMAAAAAAAAAEmVwaGVtZXJhbF9wdWJfa2V5cwAAAAAD6gAAA+4AAAAgAAAAAAAAAAltZXRhZGF0YXMAAAAAAAPqAAAADgAAAAAAAAAHYW1vdW50cwAAAAPqAAAACwAAAAEAAAPpAAAD7QAAAAAAAAfQAAAAC1NlbmRlckVycm9yAA==", - "AAAABAAAACxFcnJvcnMgdGhhdCB0aGUgc2VuZGVyIGNvbnRyYWN0IGNhbiBwcm9kdWNlLgAAAAAAAAALU2VuZGVyRXJyb3IAAAAAAwAAACpUaGUgY29udHJhY3QgaGFzIGFscmVhZHkgYmVlbiBpbml0aWFsaXNlZC4AAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAqVGhlIGNvbnRyYWN0IGhhcyBub3QgYmVlbiBpbml0aWFsaXNlZCB5ZXQuAAAAAAAOTm90SW5pdGlhbGl6ZWQAAAAAAAIAAAAwVGhlIGJhdGNoIGlucHV0IHZlY3RvcnMgaGF2ZSBtaXNtYXRjaGVkIGxlbmd0aHMuAAAADkxlbmd0aE1pc21hdGNoAAAAAAAD" ]), + "AAAAAAAAARVUcmFuc2ZlciB0b2tlbnMgYW5kIGVtaXQgYW5ub3VuY2VtZW50cyBmb3IgZW50cmllcyBwYWlkIGZvciBieSBhIHNwb25zb3IuCgpUaGUgc3BvbnNvciBhbmQgZXZlcnkgZW50cnkgc2VuZGVyIG11c3QgYXV0aG9yaXNlIHRoZSBpbnZvY2F0aW9uLiBUaGUKc3BvbnNvciBwYXlzIHRoZSB0cmFuc2FjdGlvbiBmZWUgdGhyb3VnaCBTdGVsbGFyIGZlZS1idW1wIG1lY2hhbmljczsKZW50cnkgc2VuZGVycyByZW1haW4gcmVzcG9uc2libGUgZm9yIHRoZWlyIG93biB0b2tlbiB0cmFuc2ZlcnMuAAAAAAAAEnNwb25zb3JlZF9hbm5vdW5jZQAAAAAAAgAAAAAAAAAHc3BvbnNvcgAAAAATAAAAAAAAAAdlbnRyaWVzAAAAA+oAAAfQAAAADlNwb25zb3JlZEVudHJ5AAAAAAABAAAD6QAAA+0AAAAAAAAH0AAAAAtTZW5kZXJFcnJvcgA=", + "AAAAAQAAAKpXcmFpdGggUHJvdG9jb2wgc3RhbmRhcmQgbWV0cmljIGV2ZW50IHNjaGVtYS4KCkFsbCBXcmFpdGggY29udHJhY3RzIGVtaXQgbWV0cmljIGV2ZW50cyB1c2luZyB0aGlzIHN0cnVjdHVyZSB0byBlbmFibGUKc3RhbmRhcmRpemVkIG9mZi1jaGFpbiBvYnNlcnZhYmlsaXR5IGFuZCBtb25pdG9yaW5nLgAAAAAAAAAAABFXcmFpdGhNZXRyaWNFdmVudAAAAAAAAAQAAABAQ29udHJhY3QgaWRlbnRpZmllciAoZS5nLiwgInN0ZWFsdGgtcmVnaXN0cnkiLCAic3RlYWx0aC1zZW5kZXIiKQAAAAhjb250cmFjdAAAABEAAABLT3B0aW9uYWwgZGltZW5zaW9ucyBmb3IgZmlsdGVyaW5nL2dyb3VwaW5nIChlLmcuLCB0b2tlbl9hZGRyZXNzLCBzY2hlbWVfaWQpAAAAAApkaW1lbnNpb25zAAAAAAPqAAAD7QAAAAIAAAARAAAAAAAAADNNZXRyaWMgbmFtZSAoZS5nLiwgInJlZ2lzdGVyX2NvdW50IiwgInNlbmRfdm9sdW1lIikAAAAAC21ldHJpY19uYW1lAAAAABEAAAAbTnVtZXJpYyB2YWx1ZSBvZiB0aGUgbWV0cmljAAAAAAV2YWx1ZQAAAAAAAAs=" ]), options ) } public readonly fromJSON = { init: this.txFromJSON>, send: this.txFromJSON>, - batch_send: this.txFromJSON> + batch_send: this.txFromJSON>, + sponsored_announce: this.txFromJSON> } } \ No newline at end of file diff --git a/stellar/integration-tests/sponsored.rs b/stellar/integration-tests/sponsored.rs new file mode 100644 index 0000000..9f5179a --- /dev/null +++ b/stellar/integration-tests/sponsored.rs @@ -0,0 +1,40 @@ +//! Futurenet verification for a sponsored announcement transaction. +//! +//! Submit a fee-bumped `sponsored_announce` transaction with the Stellar CLI or +//! client SDK, then run this test with `FUTURENET_TX_HASH` and +//! `FUTURENET_SPONSOR_ADDRESS` set. The test verifies the network recorded the +//! sponsor as the transaction fee source. + +use reqwest::Client; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Transaction { + successful: bool, + fee_account: String, +} + +#[tokio::test] +#[ignore = "requires a submitted futurenet fee-bumped transaction"] +async fn sponsored_announcement_fee_is_charged_to_sponsor() { + let transaction_hash = std::env::var("FUTURENET_TX_HASH") + .expect("FUTURENET_TX_HASH must identify the submitted transaction"); + let sponsor = std::env::var("FUTURENET_SPONSOR_ADDRESS") + .expect("FUTURENET_SPONSOR_ADDRESS must identify the fee-bump source"); + let horizon = std::env::var("FUTURENET_HORIZON_URL") + .unwrap_or_else(|_| "https://horizon-futurenet.stellar.org".to_owned()); + + let transaction: Transaction = Client::new() + .get(format!("{horizon}/transactions/{transaction_hash}")) + .send() + .await + .expect("Horizon request failed") + .error_for_status() + .expect("Horizon rejected the transaction lookup") + .json() + .await + .expect("Horizon returned invalid transaction JSON"); + + assert!(transaction.successful, "sponsored transaction failed"); + assert_eq!(transaction.fee_account, sponsor); +}