Skip to content

fix(phase-25/#148): soft-accept stake-cap on gossip ingress#149

Merged
clearclown merged 2 commits into
mainfrom
phase-25/148-gossip-soft-stake
May 23, 2026
Merged

fix(phase-25/#148): soft-accept stake-cap on gossip ingress#149
clearclown merged 2 commits into
mainfrom
phase-25/148-gossip-soft-stake

Conversation

@clearclown

Copy link
Copy Markdown
Owner

Summary

  • The pre-fix gossip-receive gate hard-rejected every `InferenceIneligible` verdict, including `StakeRequired` — a local-policy check that depends on the receiver's view of `StakingPool`. That view is incomplete (no canonical stake-state gossip), so the gate partitioned the mesh.
  • Observed in live 2-seed / 35-worker run: mac-studio seed rejected 100 % of gossip from asus seed once asus crossed the 10 TRM stakeless cap, while asus's own ledger climbed past 120 trades.
  • Fix: hard-reject only `PreviouslySlashed` (constitutional ban); soft-accept `StakeRequired` with a DEBUG log line. The HTTP/outbound path continues to enforce the stake gate at the source.

Closes #148.

Test plan

  • `cargo test --workspace --lib` — 1 533 passing, all 6 `pipeline::gossip_gate_tests` GREEN including the renamed `gate_enabled_soft_accepts_past_cap_provider_without_stake_or_loan`.
  • Roll the binary to the live mesh; confirm `grep "Rejected gossip trade" ~/.tirami/tirami-node.log` stops growing while `/v1/tirami/network total_trades` on the secondary seed converges with the primary's.

🤖 Generated with Claude Code

clearclown and others added 2 commits May 23, 2026 20:11
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>
Copilot AI review requested due to automatic review settings May 23, 2026 11:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 rejecting PreviouslySlashed.
  • 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 identity to 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>>,
@clearclown
clearclown merged commit 6069477 into main May 23, 2026
2 checks passed
@clearclown
clearclown deleted the phase-25/148-gossip-soft-stake branch May 23, 2026 12:58
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>
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.

bug: stakeless earn cap (10 TRM) blocks mesh trade convergence at low volume

2 participants