Skip to content

feat: harden batch-sender (pause, admin, typed errors, multisig) - #175

Merged
truthixify merged 17 commits into
wraith-protocol:developfrom
Eltimios:security/issue-155-batch-sender-production-hardening-pass-pause
Aug 28, 2026
Merged

feat: harden batch-sender (pause, admin, typed errors, multisig)#175
truthixify merged 17 commits into
wraith-protocol:developfrom
Eltimios:security/issue-155-batch-sender-production-hardening-pass-pause

Conversation

@Eltimios

@Eltimios Eltimios commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR completes the production-hardening pass for stealth-batch-sender (issue #155): a one-time init(admin, announcer, asset_policy) flow, admin-guarded pause/unpause/is_paused, a typed BatchSenderError surface replacing every raw panic! in the contract, and a 7-day timelocked signer-rotation flow (propose_/approve_/execute_/cancel_rotate_signers) mirroring stealth-sender. batch_send remains withdrawal-free and is now guarded by require_not_paused().

Note on history: an earlier push to this branch contained placeholder TODO text instead of real code in several files (caught by @truthixify's review — thank you for flagging it). Everything below reflects what's actually implemented and locally tested as of the latest commits, not what an earlier, inaccurate version of this description claimed.

Related Issue

Closes #155.

Changes

🛡️ Production-Hardening Core — stellar/stealth-batch-sender/src/lib.rs

  • Idempotent init(admin, announcer, asset_policy) with AlreadyInitialized typed error.
  • Paused storage plus admin-guarded pause, unpause, and is_paused, mirroring the stealth-sender pattern.
  • Replaces every raw panic! with a BatchSenderError enum (codes 1300-1316, the range reserved for this contract in ERRORS.md).
  • Guards batch_send with require_not_paused(); no withdrawal path exists in this contract, so nothing was left unguarded.
  • Open question for review: announcer is stored at init but not yet invoked by batch_send (which still emits its own direct ANNOUNCE event, unchanged from the original). Wiring in an announcer-contract call would be a new on-chain integration — flagging it rather than adding it silently.

🔐 Signer-Set Multisig Rotation — stellar/stealth-batch-sender/src/multisig.rs

  • Reuses the stealth-sender/src/multisig.rs shape for signer-set storage and validation.
  • propose_rotate_signers, approve_rotate_signers, execute_rotate_signers, cancel_rotate_signers, with a 7-day timelock (ROTATION_TIMELOCK_SECS).
  • Adversarial guards for non-signer proposals, duplicate approvals, early execution, invalid signer/threshold combinations, and cancel-then-repropose.

🧪 Tests

  • stellar/stealth-batch-sender/src/test.rs — rewritten to 9 result-based tests (try_* + Err(Ok(...)) assertions) instead of #[should_panic], matching the new typed-error surface.
  • stellar/stealth-batch-sender/tests/production.rs (new) — 20 acceptance tests covering init idempotency, pause/unpause + paused-call rejection, the full typed-error surface (including asset-policy allow/deny), and signer-rotation happy + adversarial paths.
  • All 29 tests pass locally, along with cargo fmt --all --check.

📄 Docs

  • stellar/PAUSE.md — batch-sender added to the pause support matrix and guarded-surface list.
  • stellar/MULTISIG.md — batch-sender added to the on-chain signer-rotation section.
  • stellar/ERRORS.md (new on this fork) — full error-code catalog including the 1300-1316 batch-sender range. Heads up: GitHub is showing a merge conflict on this file against wraith-protocol:develop — this fork was behind upstream and didn't have ERRORS.md at all until this PR added it, so it'll need reconciling with upstream's copy before merge.

Known gaps — not yet done

I don't have a working wasm32-unknown-unknown Rust toolchain in the environment I used to prepare this, so the following from the original acceptance criteria are not complete, and I didn't want to fabricate numbers for them:

  • Optimized wasm build and size measurement (no stellar/SIZE.md entry yet).
  • ABI snapshot (stellar/abi/stealth_batch_sender.json).
  • Regenerated TypeScript bindings (stellar/bindings/typescript/stealth-batch-sender/).

These correspond to CI's wasm-build/size-check/ABI-snapshot/bindings steps, which are continue-on-error: true in .github/workflows/ci.yml per the existing soroban-sdk/rustc compatibility note, so they won't block CI — but they should still be done before merge. Commands to run once a working toolchain is available:

rustup target add wasm32-unknown-unknown
stellar contract build --package stealth-batch-sender
stellar contract optimize --wasm target/wasm32-unknown-unknown/release/stealth_batch_sender.wasm
# then update stellar/SIZE.md, stellar/abi/stealth_batch_sender.json, and regenerate TS bindings

Verification Results

cargo test -p stealth-batch-sender
✅ 29/29 tests passed
cargo fmt --all --check
✅ clean

Hardening acceptance check:

  • ✅ init idempotent + AlreadyInitialized
  • ✅ pause / unpause + paused-call rejection
  • ✅ typed BatchSenderError surface for every former panic! branch
  • ✅ 7-day multisig rotation happy + adversarial paths
  • ❌ optimized wasm size / ABI snapshot / TypeScript bindings — not done yet, see "Known gaps" above

@truthixify

Copy link
Copy Markdown
Contributor

Thanks @Eltimios. The stellar job fails before it reaches any test, on a parse error:

error: expected one of `!` or `::`, found `<eof>`
 --> stellar/stealth-batch-sender/tests/production.rs:1:1
1 | TODO

tests/production.rs contains the literal text TODO, which cargo tries to compile as Rust. Either fill it in or delete the file for now and the job will get past this.

Worth flagging the bigger picture too. Issue #155 is the L-tier hardening pass: init with a typed AlreadyInitialized, Paused storage with pause / unpause / is_paused, replacing every panic! in lib.rs with a BatchSenderError enum, and the signer-set rotation with the 7-day timelock mirroring stealth-sender/src/multisig.rs. This PR is +23/-14 across 5 files, so it reads as a skeleton rather than the finished piece.

That is fine if it is early, just push the rest before marking it ready. If any part of the scope is unclear, particularly the multisig rotation, ask here and I will point you at the exact pattern to copy.

…ith-protocol#155)

Adds init(admin, announcer, asset_policy), admin-gated pause/unpause/is_paused, and the BatchSenderError enum (codes 1300-1316, reserved range per ERRORS.md) replacing every panic! in batch_send. Verified locally: 9 unit tests in src/test.rs pass.

Multisig, tests, and docs land in follow-up commits to this same PR.
This module implements an on-chain multi-sig quorum and timelock signer-rotation flow, allowing governance signers to propose new signer sets and thresholds, requiring quorum approval and a timelock before execution.
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@Eltimios Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…ith-protocol#155)

Added production-hardening acceptance tests for stealth-batch-sender, covering various scenarios including initialization, pause/unpause functionality, and error handling for batch sending.
…th-protocol#155)

Updated the On-Chain Signer Rotation section to include 'stealth-batch-sender' and clarify governance details.
This document serves as a comprehensive catalog of error codes for Stellar contracts, detailing their meanings and the contracts they belong to. It includes information on code allocation policies and existing error codes across various contracts.
@Eltimios

Copy link
Copy Markdown
Contributor Author

@truthixify — you're right, and thanks for catching it. That first push had tests/production.rs (and a couple of other files) sitting as literal TODO placeholders instead of real code; it should never have gone up looking finished. I've since rewritten the PR description to stop overclaiming and pushed the actual implementation: a typed BatchSenderError replacing every panic!, init/pause/unpause/is_paused, and the 7-day timelocked signer-rotation mirroring stealth-sender/src/multisig.rs, plus src/test.rs (9 result-based tests) and a new tests/production.rs (20 acceptance tests) — 29/29 passing locally.

One open design question I'd like your read on: announcer is stored at init, but batch_send doesn't actually call it — it still emits its own direct ANNOUNCE event, same as before this pass. Should batch_send be wired to invoke the announcer contract, or is the direct event intentional/sufficient here? Didn't want to guess and add a new on-chain call silently.

Also flagging: stellar/ERRORS.md didn't exist on this fork until this PR added it (the fork was behind develop), so GitHub is showing a merge conflict on that file against wraith-protocol:develop — it'll need reconciling before merge.

Added a comment to trigger CI validation for error catalog.
Added comments to trigger CI validation for error catalog.
@Eltimios

Copy link
Copy Markdown
Contributor Author

Quick heads-up on CI status for this PR: the last three pushes (0c7200d, 0caf2e3, d453d18) have not triggered any Actions run at all — not queued, not pending, just nothing, confirmed via the Actions API (repo-wide run count hasn't moved past #284 since ~16:44 UTC today). Every earlier push on this branch (through a751275) triggered CI normally.

The most recent real result (run #283, on a751275) fails on "Check Stellar error catalog" because at that point in history stellar/ERRORS.md didn't exist yet — that's expected and stale. I've since hand-verified that the current ERRORS.md matches every BatchSenderError variant in stellar-batch-sender/src/lib.rs (all 17, codes 1300-1316) byte-for-byte, so this check should pass once it actually runs.

Since three consecutive pushes failed to dispatch a run at all, this looks like a repo/org-level Actions issue (possibly a spending limit or quota) rather than random bad luck — worth a maintainer checking Settings → Actions and Settings → Billing when convenient. Happy to push again once that's sorted, or someone with admin access can just re-run manually.

Netty-kun and others added 3 commits August 28, 2026 22:03
…-protocol#155)

The production-hardening pass added a required init(admin, announcer,
asset_policy) flow. batch_send now returns NotInitialized until init()
is called, which broke the chaos harness test. Call init() after
registering the contract so batch_send runs as intended.
…raith-protocol#155)

measure_batch now calls init(admin, announcer, asset_policy) before
batch_send, matching the new required-init flow, and the regenerated
bench data reflects the added init/pause checks.
@truthixify
truthixify merged commit ac1d79f into wraith-protocol:develop Aug 28, 2026
13 checks passed
@truthixify

Copy link
Copy Markdown
Contributor

Merged, and thank you for turning this around properly @Eltimios. tests/production.rs went from the word TODO to 540 lines, plus the typed BatchSenderError enum replacing every panic, the signer rotation in a new multisig.rs, and the pause guards. You also picked up the parts that are easy to forget: the ABI snapshot, the regenerated TypeScript bindings, the PAUSE and MULTISIG matrices, and a chaos harness entry. That is the full scope of #155.

@Eltimios

Copy link
Copy Markdown
Contributor Author

It’s a pleasure working on this issue @truthixify

Merged, and thank you for turning this around properly @Eltimios. tests/production.rs went from the word TODO to 540 lines, plus the typed BatchSenderError enum replacing every panic, the signer rotation in a new multisig.rs, and the pause guards. You also picked up the parts that are easy to forget: the ABI snapshot, the regenerated TypeScript bindings, the PAUSE and MULTISIG matrices, and a chaos harness entry. That is the full scope of #155.

@truthixify truthixify mentioned this pull request Aug 31, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Batch-sender production-hardening pass (pause, admin, typed errors, multisig rotation)

3 participants