fix(phase-25/#148): soft-accept stake-cap on gossip ingress#149
Merged
Conversation
Same 32-byte seed underlies both the iroh QUIC keypair AND HTTP-layer trade/loan signing. Treat it as a Bitcoin-style wallet: * `WalletKey::load_or_create` auto-creates `~/.tirami/node.key` with file mode 0600 on first run; atomic write via sibling-tmp + rename * World/group-readable existing files trigger a runtime warning * `TiramiNode.wallet: Option<Arc<WalletKey>>` is populated by `init_transport` and threaded onto `AppState` * `forge_borrow` now binds the loan to the *real* node identity (the wallet) instead of a one-shot ephemeral key, so /v1/tirami/borrow actually accrues credit + effective balance for the calling node; `forge_lend_to` does the same on the lender side * `tirami wallet identity` CLI prints NodeId, wallet path, and file permissions without touching the Lightning bootstrap path * Drops the now-redundant `load_node_secret_key` / `parse_node_secret_key_bytes` helpers in `node.rs` (replaced by `wallet::parse_seed_bytes` covered by 8 dedicated tests) Tests: 8 new wallet tests + workspace-wide lib suite (1 533 passed). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The pre-fix `check_gossip_trade_eligibility` authoritatively rejected *any* `InferenceIneligible` verdict on inbound gossip — including `StakeRequired`, which is a local-policy check that depends on the receiver's view of `StakingPool`. That view is incomplete by construction (no canonical stake-state gossip in this protocol revision), so a receiver rejecting based on it partitions the mesh. Observed in a live 2-seed / 35-worker run (issue #148): mac-studio seed dropped 100 % of gossip from asus seed within seconds of asus crossing the 10 TRM stakeless cap, while asus's own ledger view climbed past 120 trades. `total_nodes` on mac-studio stayed at 3 while asus saw 17 — exactly the partition the docstring warned against. Fix: * Hard-reject `PreviouslySlashed` (constitutional permanent ban — this is the one verdict every node legitimately enforces locally). * Soft-accept `StakeRequired` — log at DEBUG so the local-policy mismatch is observable, but still record the trade. The HTTP/outbound path (`forge_chat_completions` and friends) continues to enforce the stake gate at the source. Tests: * Renamed `gate_enabled_rejects_past_cap_provider_without_stake_or_loan` → `gate_enabled_soft_accepts_past_cap_provider_without_stake_or_loan`. Asserts the underlying ledger gate still reports `StakeRequired` (HTTP-path unchanged) AND the gossip-receive helper now soft-accepts. * Existing `gate_enabled_rejects_previously_slashed_provider` confirms the constitutional ban remains hard. Closes #148. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses mesh partitioning caused by applying a local stake-cap eligibility check to inbound gossip trades, by soft-accepting StakeRequired on gossip ingress while still hard-rejecting PreviouslySlashed. In addition, it introduces a persistent Ed25519 “wallet” abstraction and plumbs it through node initialization, HTTP signing paths, and CLI tooling.
Changes:
- Soft-accept inbound gossip trades that fail eligibility with
InferenceIneligible::StakeRequired, while still rejectingPreviouslySlashed. - Add
WalletKey(persistent Ed25519 seed/key handling) and wire it into node transport init and HTTP loan/trade signing paths. - Extend CLI with
tirami wallet identityto display (and auto-create) the node wallet identity.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/tirami-node/src/wallet.rs | Adds persistent wallet abstraction (load/generate/parse/write + tests). |
| crates/tirami-node/src/pipeline.rs | Soft-accepts StakeRequired on gossip ingress; updates unit tests/docs. |
| crates/tirami-node/src/node.rs | Loads/creates wallet during transport init and passes it into API state. |
| crates/tirami-node/src/api.rs | Adds wallet to AppState and uses it for borrower/lender signing when present. |
| crates/tirami-node/src/security_tests.rs | Updates router construction for new wallet parameter. |
| crates/tirami-node/src/lib.rs | Exposes new wallet module. |
| crates/tirami-cli/src/main.rs | Adds wallet identity subcommand + helper for default node key path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+152
to
+168
| fn write_seed_atomic(path: &Path, seed: &[u8; 32]) -> io::Result<()> { | ||
| // Write to a sibling tmp file first, fsync-via-rename so a crash | ||
| // between create and write can never leave a half-written wallet. | ||
| let tmp = match path.file_name() { | ||
| Some(name) => { | ||
| let mut buf = std::ffi::OsString::from(name); | ||
| buf.push(".tmp"); | ||
| path.with_file_name(buf) | ||
| } | ||
| None => path.with_extension("key.tmp"), | ||
| }; | ||
| fs::write(&tmp, seed)?; | ||
| #[cfg(unix)] | ||
| fs::set_permissions(&tmp, fs::Permissions::from_mode(0o600))?; | ||
| fs::rename(&tmp, path)?; | ||
| Ok(()) | ||
| } |
Comment on lines
+449
to
+452
| // Phase 25 #161 — Identity action is purely read-only on | ||
| // the node-key file. Handle it before bringing up the | ||
| // Lightning wallet (which opens disk + network resources) | ||
| // so `tirami wallet identity` stays fast and side-effect-free. |
Comment on lines
+69
to
+75
| /// Phase 25 #161 — persistent Ed25519 wallet. Same 32-byte seed | ||
| /// underlies the iroh QUIC keypair AND HTTP-layer trade/loan | ||
| /// signing. Populated by `init_transport` (loaded from | ||
| /// `config.node_key_path` or generated in-memory). Threaded onto | ||
| /// `AppState` so handlers like `/v1/tirami/borrow` sign as the | ||
| /// real node identity instead of one-shot ephemeral keys. | ||
| pub wallet: Option<Arc<WalletKey>>, |
This was referenced May 23, 2026
clearclown
added a commit
that referenced
this pull request
May 23, 2026
The HTTP path `forge_chat_completions` at `crates/tirami-node/src/api.rs:1935` returns `403 stake_gate_denied` when the provider has crossed `STAKELESS_EARN_CAP_TRM=10` without locking `MIN_PROVIDER_STAKE_TRM=100`. The P2P path `handle_inference` at `crates/tirami-node/src/pipeline.rs` did not enforce the same gate, so worker meshes drove the seed past the cap indefinitely. Observed in the live 2-seed / 35-worker mesh (see issue #151): * HTTP: 403 with `stake_gate_denied` (seed at 8 648 TRM contributed, 0 TRM staked, 100 TRM required). * P2P: trade timestamps sub-second-old, count growing ~30/min. This defeats the Constitutional gate's purpose (Sybil resistance + economic disincentive against malicious providers). Fix: * Plumb `staking_pool` through the pipeline-run-seed dispatch into `handle_inference`. * Mirror the HTTP gate at the top of `handle_inference`, right after request validation and before CU reservation. Failure returns an `ErrorCode::InvalidRequest` Error with `stake_gate_denied: <verdict>`, non-retryable. * `config.stake_gate_enabled = false` keeps the legacy behaviour for operators who opt out at boot. The receiver-side gossip gate (#148 / PR #149) remains soft-accept: *that* path can't authoritatively reject because the receiver lacks the provider's canonical stake state. This patch closes the gate where it belongs — at the provider, where the stake state is authoritative and known. Closes #151. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #148.
Test plan
🤖 Generated with Claude Code