Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions stellar/PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions stellar/bench/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<stealth_sender::SponsoredEntry> = 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",
Expand Down
83 changes: 75 additions & 8 deletions stellar/bindings/typescript/stealth-sender/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<readonly [string, any]>;
/**
* 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<AssembledTransaction<Result<void>>>
init: ({announcer, asset_policy, fee_recipient, fee_basis_points}: {announcer: string, asset_policy: Option<string>, fee_recipient: Option<string>, fee_basis_points: u32}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>

/**
* 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.
Expand All @@ -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<string>, ephemeral_pub_keys: Array<Buffer>, metadatas: Array<Buffer>, amounts: Array<i128>}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>

/**
* 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<SponsoredEntry>}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>

}
export class Client extends ContractClient {
static async deploy<T = Client>(
Expand All @@ -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<Result<void>>,
send: this.txFromJSON<Result<void>>,
batch_send: this.txFromJSON<Result<void>>
batch_send: this.txFromJSON<Result<void>>,
sponsored_announce: this.txFromJSON<Result<void>>
}
}
40 changes: 40 additions & 0 deletions stellar/integration-tests/sponsored.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading