MPP - Channel Mode

Verified against @stellar/mpp@0.7.1, mppx@0.8.14, @stellar/stellar-sdk@16.1.0.

IDCheck NameDestructiveSpec ReferenceWhat It ChecksPass Criteria
MPP-10Channel DeploynoMPP Channel GuideThe channel contract deploys correctly, and is the same channel the target bills throughContract address is valid and its state is queryable via getChannelState(), matching the parameters it was opened with (token, from, to, refundWaitingPeriod). The channel inspected is the one the target advertises in its 402 challenge, so MPP-10 and MPP-11/12/13/14 always report on the same contract. --channel (env CHANNEL_CONTRACT) asserts an expected address rather than selecting one: when it differs from the advertised channel the check fails and inspects nothing, because a run that reported on both would be reporting on two contracts at once. If the challenge cannot be read at all, an explicitly named channel is still inspected and the result is marked unverified against the target.
MPP-11Cumulative Commitment OrderingnoMPP Channel Guide §closing-the-channel; @stellar/mpp channel server (cumulativeMonotonicityError)Both ordering rules: a commitment must exceed the stored cumulative, and must cover the price of the current requestTwo probes, each rejected with HTTP 402: one committing exactly the stored cumulative, one advancing but falling short of cumulative + price. Each probe uses a fresh challenge, so the earlier-running challenge replay guard cannot fire and be mistaken for ordering enforcement. The second probe is reported as not-probed when the price is 1 base unit, since it collapses into the first.
MPP-12Challenge Replay Rejection (negative)noMPP Channel Guide §closing-the-channel; @stellar/mpp channel server (atomic compare-and-set on challenge ID)A byte-identical credential resubmitted against the same challenge must be rejectedHTTP 402 on the second submission. Isolation: the replayed credential is identical to one the server accepted moments earlier, so its signature and amount are already proven valid and only the challenge-ID claim can explain the rejection.
MPP-14Commitment Replay Rejection (negative)noMPP Channel Guide §closing-the-channel; @stellar/mpp channel server (cumulativeMonotonicityError)A captured (amount, signature) pair must not be redeemable against a new challengeHTTP 402 when a previously accepted commitment is re-presented under a fresh challenge. This is the realistic double-spend: the challenge replay guard cannot help because the challenge ID is new, so only the cumulative rule stands in the way. The official client SDK cannot express this probe — it re-signs on every call.
MPP-13Close SettlementyesMPP Channel Guide §closing-the-channelClosing with the highest commitment settles on-chainServer accepts the close credential (HTTP 200), then RPC confirms settlement: closeEffectiveAtLedger moves from null to a ledger sequence, and the contract's withdrawn getter equals exactly the committed amount. The channel balance is not asserted — close() pays the commitment to the recipient and then auto-refunds the remainder to the funder, so a closed channel always ends at zero. withdrawn is written in the same call that transfers the payout, and that transfer is non-fallible, so a mismatch would have reverted the whole close. Running this permanently ends the channel — skipped unless destructive checks are explicitly enabled, and it refuses to run unless the operator names the channel and the target's challenge advertises that same address.

Reporting semantics (Week 2). Beyond PASS and SKIP, a run distinguishes two further outcomes, because "your service is broken" and "we never reached your service" are not the same claim:

  • FAIL — the target answered, and the answer did not conform. This includes a response that arrives but violates the spec: wrong status, unparseable challenge, missing channel/amount, non-numeric amounts.
  • ERROR — no verdict was produced about the target at all. Either it could not be reached (unreachable), or the run is misconfigured (configuration), or the harness itself failed (harness). An ERROR is never a statement about the target's conformance.

Exit codes follow from that: 0 every check conformed, 1 at least one conformance failure, 2 at least one check produced no verdict. A run with both exits 1, because a real finding outranks a missing one.

PREFLIGHT is not a check and carries no spec reference, which is why it has no row above. It is a single diagnostic emitted when the target URL or the network identifier is invalid — both are wrong for every check in the suite, so they are reported once rather than repeated identically per check. When preflight fails, no check runs.

Note on interfaces and access paths (Week 2). The catalogue is exercised by two interfaces over one core: the wasit CLI and the wasit-mcp MCP server. Both run identical check code against the same suite functions, so the two can never disagree about the same target. Every check in this catalogue is reachable from both: test / wasit_x402_test for x402, mpp-charge / wasit_mpp_charge_test for charge mode, and mpp-channel / wasit_mpp_channel_test for channel mode. Only the reporting surface differs — exit codes are a CLI concept, and the MCP server reports the same verdict as an outcome field in its structured output.

MPP-13 is reachable from both interfaces, but by different routes. Over MCP, wasit_mpp_channel_test runs the non-destructive channel checks (MPP-10, MPP-11, MPP-12, MPP-14) and reports MPP-13 as SKIP, so an agent can see that the check exists and why it did not run, rather than silently receiving a shorter catalogue. The destructive route is a separate tool, wasit_mpp_channel_test_with_close, which is registered only when a human starts the server with WASIT_ALLOW_DESTRUCTIVE=1 or --allow-destructive. Without that opt-in the tool is absent from tools/list altogether: an agent cannot invoke a tool it cannot see, which is a stronger guarantee than a boolean argument it could set for itself. When the tool is registered, it still requires a destructiveChannel argument naming the channel it is permitted to close, and the guard described in the MPP-13 row still applies unchanged — the run refuses unless the target's challenge advertises that same address. Signing keys are read from the server process environment and are never accepted as tool arguments, so an agent never handles them.

Revision note (Week 2, corrected): MPP-11/MPP-12 pass criteria were first written as "server rejects", then briefly revised to "zero balance delta / silent no-op" after reading only the stellar-experimental/one-way-channel on-chain contract source (which is genuinely a silent no-op for stale settle/close calls). That revision was corrected after reading the @stellar/mpp channel server implementation directly: the HTTP-facing server — the actual artifact Wasit tests — rejects stale/replayed commitments explicitly via ChannelVerificationError, before the on-chain contract is ever invoked. The contract's own no-op behavior only applies if the contract is called directly, bypassing the server, which is out of scope for Wasit.

Status note (Week 2, corrected). All thirteen checks in this catalogue are implemented and reachable from both front ends. X402-02 deliberately accepts either header name because Stellar's own official documentation is not yet internally consistent (PAYMENT-REQUIRED vs X-Payment); that divergence is a documentation defect upstream, not a choice this catalogue is making. Note that the x402Version field-name difference checked by X402-04 is not a divergence of the same kind: it is a deliberate, documented change between protocol versions, and the SDK implements it correctly.

Note on error granularity (Week 2). All channel-mode rejections return the same HTTP 402 body: {"type": ".../problems/verification-failed", "title": "Verification Failed", ...}. Replay, non-monotonic commitments, bad signatures and a settling channel are indistinguishable from the response alone. Cause: @stellar/mpp throws ChannelVerificationError, which extends StellarMppError rather than mppx's PaymentError, so mppx rewraps every one of them into a generic VerificationFailedError. mppx already defines precise types for this family — session/invalid-signature, session/signer-mismatch, session/amount-exceeds-deposit, session/delta-too-small, session/insufficient-balance, session/channel-finalized — and none are currently reachable from channel mode.

Wasit therefore isolates each rule by construction rather than by asserting on the response type: every check is built so that exactly one rejection path can fire, and the "Pass Criteria" column above records how. This is a gap in the official SDK, not in any service under test, and is documented in docs/findings/upstream-sdk.md for upstream reporting.

Note on close authorisation (Week 2). The channel contract's close() calls to.require_auth() — the recipient must authorise the close, not the funder. @stellar/mpp exposes this as feePayer.envelopeSigner, a name that implies fee payment rather than authorisation; configuring it with the funder's key produces a transaction that reaches the chain and fails there with an opaque scecInvalidAction, which the SDK surfaces as [object Object]. By contrast, close_start() requires from.require_auth() — the funder. Both details are documented in docs/findings/upstream-sdk.md for upstream reporting.