Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2c1db9a
security: Batch-sender production-hardening pass (pause, admin, typed…
Eltimios Aug 26, 2026
8d1b2f3
security: Batch-sender production-hardening pass (pause, admin, typed…
Eltimios Aug 26, 2026
01cfb5f
security: Batch-sender production-hardening pass (pause, admin, typed…
Eltimios Aug 26, 2026
39fd149
security: Batch-sender production-hardening pass (pause, admin, typed…
Eltimios Aug 26, 2026
c8ec295
security: Batch-sender production-hardening pass (pause, admin, typed…
Eltimios Aug 26, 2026
2077045
security: implement batch-sender production-hardening pass (issue #155)
Eltimios Aug 28, 2026
f65b9cf
Implement multi-sig signer rotation and governance logic
Eltimios Aug 28, 2026
ce2cc18
Rewrite batch-sender unit tests for result-based error handling
Eltimios Aug 28, 2026
cc103bf
Add production-hardening acceptance tests for batch-sender (issue #155)
Eltimios Aug 28, 2026
fb89776
Document pause posture for batch-sender (issue #155)
Eltimios Aug 28, 2026
a751275
Document batch-sender signer rotation in multisig runbook (issue #155)
Eltimios Aug 28, 2026
0c7200d
Add unified error code catalog (ERRORS.md), including batch-sender range
Eltimios Aug 28, 2026
0caf2e3
chore: retrigger CI to validate ERRORS.md against latest HEAD
Eltimios Aug 28, 2026
d453d18
chore: second CI retrigger attempt after dispatch gap
Eltimios Aug 28, 2026
863f033
fix: resolve merge conflicts with base
Netty-kun Aug 28, 2026
2935a84
test: init batch-sender before batch_send in chaos test (issue #155)
Eltimios Aug 28, 2026
2ca74b2
bench: init batch-sender before batch_send in crossover bench (issue …
Eltimios Aug 28, 2026
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
24 changes: 20 additions & 4 deletions stellar/ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Error codes are only unique within a contract enum at the Soroban ABI layer, but
| `stealth-announcer` | none | Panic-only/stateless event emitter today. Reserve `1000-1099` if it gains `#[contracterror]`. |
| `stealth-registry` / `RegistryError` | `1100-1199` | Existing deployed codes are `1-2`; do not renumber them. Add future variants in the reserved range unless a breaking ABI migration is planned. |
| `stealth-sender` / `SenderError` | `1200-1299` | Existing deployed codes are `1-16`; do not renumber them. |
| `stealth-batch-sender` | `1300-1399` | Panic-only today; use this range when issue #1 converts panics to `#[contracterror]`. |
| `stealth-batch-sender` / `BatchSenderError` | `1300-1399` | Existing deployed codes are `1300-1316`; do not renumber them. |
| `stealth-vault` / `VaultError` | `1400-1499` | Existing codes are `1-7`; do not renumber them. |
| `stealth-splitter` / `SplitterError` | `1500-1599` | Existing codes are `1-8`; do not renumber them. |
| `wraith-names` / `NamesError` | `1600-1699` | Existing codes are `1-32`; do not renumber them. New variants use the reserved range, starting at `1600`. |
Expand Down Expand Up @@ -61,11 +61,25 @@ No `#[contracterror]` enum is defined. Current validation failures are panics/as

## stealth-batch-sender

No `#[contracterror]` enum is defined. This contract is currently panic-only; issue #1 tracks conversion to structured errors.

| Code | Name | Meaning | Introduced in |
|---:|---|---|---|
| N/A | Panic-only | Empty batches, oversized batches, non-positive amounts, empty ephemeral keys, and token failures currently abort by panic/host error. | pre-catalog |
| 1300 | [`BatchSenderError::AlreadyInitialized`](stealth-batch-sender/src/lib.rs#L50) | Contract initialization was attempted more than once. | issue #155 |
| 1301 | [`BatchSenderError::NotInitialized`](stealth-batch-sender/src/lib.rs#L52) | `batch_send` was called before `init`. | issue #155 |
| 1302 | [`BatchSenderError::EmptyBatch`](stealth-batch-sender/src/lib.rs#L54) | Batch contains no transfers. | issue #155 |
| 1303 | [`BatchSenderError::BatchTooLarge`](stealth-batch-sender/src/lib.rs#L56) | Batch exceeds `MAX_BATCH_SIZE` (100). | issue #155 |
| 1304 | [`BatchSenderError::NonPositiveAmount`](stealth-batch-sender/src/lib.rs#L58) | A transfer amount is zero or negative. | issue #155 |
| 1305 | [`BatchSenderError::EmptyEphemeralKey`](stealth-batch-sender/src/lib.rs#L60) | A transfer's ephemeral public key is empty. | issue #155 |
| 1306 | [`BatchSenderError::Paused`](stealth-batch-sender/src/lib.rs#L62) | Contract is paused. | issue #155 |
| 1307 | [`BatchSenderError::AssetNotAllowed`](stealth-batch-sender/src/lib.rs#L64) | Configured asset policy rejected the token. | issue #155 |
| 1308 | [`BatchSenderError::MultisigNotInitialized`](stealth-batch-sender/src/lib.rs#L66) | Governance multisig has not been initialized. | issue #155 |
| 1309 | [`BatchSenderError::MultisigAlreadyInitialized`](stealth-batch-sender/src/lib.rs#L68) | Governance multisig initialization was attempted more than once. | issue #155 |
| 1310 | [`BatchSenderError::NotSigner`](stealth-batch-sender/src/lib.rs#L70) | Caller is not a current governance signer. | issue #155 |
| 1311 | [`BatchSenderError::InvalidThreshold`](stealth-batch-sender/src/lib.rs#L72) | Requested multisig threshold is zero or exceeds signer count. | issue #155 |
| 1312 | [`BatchSenderError::RotationAlreadyPending`](stealth-batch-sender/src/lib.rs#L74) | A signer-rotation proposal already exists. | issue #155 |
| 1313 | [`BatchSenderError::NoPendingRotation`](stealth-batch-sender/src/lib.rs#L76) | No signer-rotation proposal exists for this action. | issue #155 |
| 1314 | [`BatchSenderError::AlreadyApprovedRotation`](stealth-batch-sender/src/lib.rs#L78) | Caller already approved the pending rotation. | issue #155 |
| 1315 | [`BatchSenderError::QuorumNotMet`](stealth-batch-sender/src/lib.rs#L80) | Pending rotation lacks enough approvals. | issue #155 |
| 1316 | [`BatchSenderError::TimelockNotElapsed`](stealth-batch-sender/src/lib.rs#L82) | Rotation timelock has not elapsed. | issue #155 |

## stealth-vault

Expand Down Expand Up @@ -199,3 +213,5 @@ These enums are compiled only for tests or live under test fixtures. They are in
| 1 | [`MockTokenError::InsufficientBalance`](stealth-sender/src/test_mocks.rs#L26) | Mock token transfer balance is too low. | pre-catalog |
| 2 | [`MockTokenError::InsufficientAllowance`](stealth-sender/src/test_mocks.rs#L27) | Mock token allowance is too low. | pre-catalog |
| 11 | [`TokenError::BalanceDeauthorized`](stealth-sender/tests/mocks/token_auth_required.rs#L12) | Test token recipient is not authorized to receive balance. | pre-catalog |
<!-- ci-retrigger: validate error catalog after ERRORS.md's addition to this branch -->
<!-- ci-retrigger: attempt 2, dispatch gap on prior push -->
6 changes: 5 additions & 1 deletion stellar/MULTISIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,17 @@ Commit `multisig-setup.log` to your ops runbook repo (not this repository) after

---

## On-Chain Signer Rotation (`stealth-sender`, `wraith-names`)
## On-Chain Signer Rotation (`stealth-sender`, `stealth-batch-sender`, `wraith-names`)

The account-level Stellar multisig above governs the *admin key* that submits
transactions. Separately, `stealth-sender` and `wraith-names` β€” the two
contracts GOVERNANCE.md marks **Timelock + Multisig Upgradable** β€” each keep
their own on-chain governance signer set and quorum threshold, used to
authorise a `rotate_signers` flow without a contract redeploy (issue #104).
`stealth-batch-sender` reuses the identical `propose_/approve_/execute_/
cancel_rotate_signers` shape and 7-day timelock as of issue #155, though
GOVERNANCE.md itself has not yet been updated to classify it alongside
`stealth-sender`/`wraith-names` β€” worth a follow-up issue.

This is intentionally a second, independent layer: the Stellar account
multisig above controls *who can submit transactions at all*; the contract's
Expand Down
10 changes: 9 additions & 1 deletion stellar/PAUSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ data: `(caller,)`).
| stealth-announcer | No | Stateless event emitter β€” no storage, nothing to pause |
| stealth-registry | No | Not implemented; non-custodial metadata writes, registrations are not guarded |
| stealth-sender | Yes | Moves tokens; pause prevents sends during incident |
| stealth-batch-sender | Yes | Moves tokens; pause prevents batch sends during incident |
| wraith-names | Yes | Name registry with ownership; pause prevents registrations, updates, releases, and TTL extensions |
| stealth-vault | Yes | Custodies time-locked deposits; pause prevents new deposits during an incident |

Expand All @@ -30,6 +31,13 @@ Guarded by `require_not_paused`:
NOT guarded (users must be able to exit during an incident):
- `withdraw_many` β€” batch asset exits

### stealth-batch-sender

Guarded by `require_not_paused`:
- `batch_send` β€” batch token transfers + announcements

There is no withdrawal path in this contract (see stealth-batch-sender/README.md), so there is nothing to leave unguarded for exits.

### stealth-vault

Guarded by `require_not_paused`:
Expand Down Expand Up @@ -78,4 +86,4 @@ env.events().publish(("paused",), (caller,));

// Unpause
env.events().publish(("unpaused",), (caller,));
```
```
33 changes: 19 additions & 14 deletions stellar/SIZE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
WASM Size Metrics
This document tracks the optimized Soroban contract WASM payloads. The CI budget is
110,000 bytes (the workflow allows 112,640 bytes to account for the 110 KiB
wording used by the network).
110,000 bytes (the workflow allows 112,640 bytes to account for the 110 KiB wording used by the network).

Release profile audit
All workspace members inherit the release profile in Cargo.toml.
Expand Down Expand Up @@ -34,9 +33,9 @@ wraith_asset_policy 14,163 6,245 55.91%
governance 39,519 21,558 45.46%
Every contract that changed is more than 10% smaller and all measured payloads
are below the 110,000-byte budget. governance has no removable symbol section
in this toolchain, so its 0% delta is the documented β€œcannot shrink further”
in this toolchain, so its 0% delta is the documented "cannot shrink further"
case; it is already 80.40% below budget. Symbol stripping is safe for these
cdylib artifacts: it removes non-executable metadata only and therefore has no
cdyli artifacts: it removes non-executable metadata only and therefore has no
runtime or storage semantics.

wraith_names is retained in the historical baseline below, but cannot be
Expand All @@ -61,18 +60,24 @@ Contract Before metrics (bytes) After metrics (bytes) Delta Growth
stealth_splitter 9,774 10,720 +946 +9.68%
stealth_vault 9,237 11,117 +1,880 +20.35%
governance 16,589 18,506 +1,917 +11.56%
wraith_names not measurable not measurable β€” β€”
wraith_names not measurable not measurable -- --
All three measurable payloads stay far below the 112,640-byte CI budget; the
largest, governance, is 83.57% below it.

wraith_names cannot be compiled for wasm32-unknown-unknown at all (see the
note above), so its metric-emission delta cannot be measured on this toolchain.
The failure reproduces identically on the parent commit, so it is unrelated to
the metric wiring. Once the soroban-sdk bump lands and the contract builds,
re-run the command below and fill the row in; the wiring adds five call sites,
wraith_names cannot be compiled for wasm32-unknown-unknown at all (see
the note above), so its metric-emission delta cannot be measured on this toolchain.
The failure reproduces identically on the parent commit, so it is unrelated to the
metric wiring. Once the soroban-sdk bump lands and the contract builds,
re-run the command below and fill the row in the wiring adds five call sites,
so it should land in the same +1 to +2 KB range as the other three.

Reproducing the per-contract delta
Batch-sender hardening pass
the stealth_batch_sender contract gained init, pause/admin, typed errors, and
signer rotation in the same shape as stealth_sender. The optimized WASM payload
(measured with strip = "symbols") is 18,662 bytes, still 83.03% below the
112,640-byte CI budget.

## Reproducing the per-contract delta
From this directory, run the same commands used by CI. Record the byte count of
each unoptimized WASM before applying the profile/optimizer, then record the
optimized output after the profile change:
Expand All @@ -83,7 +88,7 @@ cargo build --target wasm32-unknown-unknown --release
for wasm in target/wasm32-unknown-unknown/release/*.wasm; do
stellar contract optimize --wasm "$wasm"
done
find target/wasm32-unknown-unknown/release -name '*_optimized.wasm' \
find target/wasm32-unknown-unknown/release -name '*_optimized.wasm'
-printf '%f %s bytes\n' | sort
The optimizer is deliberately run on the release output, as the network deploys
the optimized payload rather than the intermediate compiler artifact. CI rejects
Expand All @@ -92,7 +97,7 @@ any optimized payload over 112,640 bytes.
A workspace-wide wasm32 build fails because integration-tests pulls
soroban-sdk with the testutils feature and Cargo unifies that feature across
the whole build. To measure a single contract, name it explicitly so the
testutils-enabled members stay out of the graph:
testutil-enabled members stay out of the graph:

Shell

Expand All @@ -103,4 +108,4 @@ for wasm in target/wasm32-unknown-unknown/release/*.wasm; do
done

Note that stellar-cli 27.x writes <name>.optimized.wasm where the 22.0.1 CLI
pinned in CI writes <name>_optimized.wasm; match the glob to the CLI in use.
pinned in CI writes <name>_optimized.wasm; match the glob to the CLI in use.
1 change: 1 addition & 0 deletions stellar/abi/stealth_batch_sender.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
4 changes: 4 additions & 0 deletions stellar/bench-crossover/src/crossover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ fn measure_batch(n: u32) -> Measured {

let contract_id = env.register(StealthBatchSender, ());
let client = StealthBatchSenderClient::new(&env, &contract_id);
// batch_send now requires init(admin, announcer, asset_policy) (issue #155).
let admin = Address::generate(&env);
let announcer = Address::generate(&env);
client.init(&admin, &announcer, &None);
let (token, sender) = funded_token(&env);

let mut transfers: SorobanVec<Transfer> = vec![&env];
Expand Down
4 changes: 2 additions & 2 deletions stellar/bench/data/crossover-chart.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ xychart-beta
title "Instructions: individual send vs batch send"
x-axis [1, 2, 5, 10, 15, 20]
y-axis "Instructions"
line "individual (N x send)" [216310, 432620, 1081550, 2163100, 3244650, 4326200]
line "batch (batch_send)" [182837, 320986, 743151, 1496019, 2292557, 3103832]
line "individual (N x send)" [223328, 446656, 1116640, 2233280, 3349920, 4466560]
line "batch (batch_send)" [198036, 336876, 761114, 1517437, 2317430, 3132160]
```
12 changes: 6 additions & 6 deletions stellar/bench/data/crossover.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
n,individual_instructions,batch_instructions,individual_instr_per_entry,batch_instr_per_entry,individual_wall_ns,batch_wall_ns,individual_wall_ns_per_entry,batch_wall_ns_per_entry,winner
1,216310,182837,216310,182837,782800,575600,782800,575600,batch
2,432620,320986,216310,160493,1002900,412000,501450,206000,batch
5,1081550,743151,216310,148630,1992000,1003000,398400,200600,batch
10,2163100,1496019,216310,149601,4135300,2783400,413530,278340,batch
15,3244650,2292557,216310,152837,7869400,6136100,524626,409073,batch
20,4326200,3103832,216310,155191,12951500,6341200,647575,317060,batch
1,223328,198036,223328,198036,400298,331589,400298,331589,batch
2,446656,336876,223328,168438,791199,496787,395599,248393,batch
5,1116640,761114,223328,152222,1916722,1109742,383344,221948,batch
10,2233280,1517437,223328,151743,3713285,2254698,371328,225469,batch
15,3349920,2317430,223328,154495,5882279,3232240,392151,215482,batch
20,4466560,3132160,223328,156608,7317857,4014791,365892,200739,batch
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
6 changes: 6 additions & 0 deletions stellar/integration-tests/tests/chaos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,12 @@ fn batch_sender_batch_send_through_chaos() {
let batch_sender_id = env.register(StealthBatchSender, ());
let client = StealthBatchSenderClient::new(&env, &batch_sender_id);

// The production-hardening pass (issue #155) added a one-time init
// flow β€” batch_send now returns NotInitialized until init() is called.
let admin = Address::generate(&env);
let announcer = Address::generate(&env);
client.init(&admin, &announcer, &None);

let (token, from) = funded_token(&env);

let stealth1 = Address::generate(&env);
Expand Down
Loading
Loading