Skip to content

fix(mpp): bind session state to confirmed channels#239

Draft
EfeDurmaz16 wants to merge 51 commits into
fix/python-security-hardeningfrom
fix/mpp-session-state-hardening
Draft

fix(mpp): bind session state to confirmed channels#239
EfeDurmaz16 wants to merge 51 commits into
fix/python-security-hardeningfrom
fix/mpp-session-state-hardening

Conversation

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator

Summary

  • binds MPP session opens and top-ups to confirmed on-chain payment-channel state across TypeScript, Go, Rust, and Python
  • validates channel discriminator/version, program ownership, mint, recipient, operator, payer, authorized signer, deposit, and confirmation slot
  • rejects spent or economically divergent channels by binding settlement watermarks, grace period, distribution hash, salt, and open slot
  • persists the actual server-broadcast open signature so retries never confirm a newly signed transaction that was not broadcast
  • requires an affirmatively declared durable shared session store outside localnet; unknown and process-local stores fail closed
  • preserves explicit development-only overrides and high-level SDK/framework injection paths

Security invariants

  • transaction confirmation alone never proves the channel account matches the challenged session
  • top-ups cannot credit a claimed deposit that differs from authoritative post-transaction state
  • production session state cannot silently fall back to an unmarked or process-local store
  • direct constructors and public adapters enforce the same RPC and store requirements
  • durable state retains both channel PDA seeds; Rust binds client-broadcast signatures to the submitted transaction and rejects unsupported partial server-broadcast opens explicitly

Verification

  • Go focused server/paykit tests
  • Python 87 focused session tests, Ruff, and Pyright
  • Rust full server,client feature suite: 932 tests
  • TypeScript full suite: 526 tests; PayKit typecheck, ESLint, and Prettier
  • independent adversarial review after the affirmative durability follow-up

Base is #219. This is a protocol PR because the same payment-channel state-binding and durability contract spans multiple SDKs.

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please review the current head.

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please re-review the current head after the confirmed-commitment fix.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR binds MPP session opens and top-ups to confirmed on-chain payment-channel state across TypeScript, Go, Rust, and Python, closing the gap where transaction confirmation alone was accepted as proof of channel validity. It also enforces a durable shared session store in production and persists server-broadcast open signatures to make retries safe.

  • TypeScript and Go: fully rewritten to use binary wire-transaction decoding (encoding=\"base64\") for both open and top-up verification, with canonical instruction re-encoding, PDA re-derivation, and strict account-slot checks; assertSignatureSucceeded now throws on a missing context slot instead of silently passing 0n.
  • Rust: process_open and process_topup now bind every persisted channel fact to the confirmed on-chain account; without an rpc_url the paths fail closed off localnet.
  • Python: the open path correctly switches to encoding=\"base64\" for signature-only opens, but new_top_up_state_tx_verifier still fetches the top-up transaction with encoding=\"jsonParsed\", which will reject all valid top-ups once the payment-channels program is registered on production RPC nodes.

Confidence Score: 3/5

The cross-language state-binding work is correct and consistent in TypeScript, Go, and Rust; the Python top-up path has an encoding mismatch that will cause all valid top-ups to fail on production RPC nodes once the payment-channels program is registered.

The Python new_top_up_state_tx_verifier function added in this PR fetches the confirmed top-up transaction with encoding="jsonParsed" while _verify_confirmed_top_up expects a base58 data string — a format that disappears when the program is registered in an RPC node's IDL database. This is a present defect on the payment-critical top-up path.

python/src/solana_pay_kit/protocols/mpp/server/session_onchain.py — specifically new_top_up_state_tx_verifier (encoding) and _verify_confirmed_top_up (instruction count constraint).

Important Files Changed

Filename Overview
python/src/solana_pay_kit/protocols/mpp/server/session_onchain.py Adds state-aware open/top-up verifiers with base64 encoding for the open path, but new_top_up_state_tx_verifier still uses encoding="jsonParsed" for the top-up path; _verify_confirmed_top_up also accepts N>1 matching instructions.
go/protocols/mpp/server/session_onchain.go Adds fetchAndValidateChannel, fetchAndBindChannelAccount, and NewTopUpStateTxVerifier using binary wire-transaction decoding; enforces exactly 1 top-up instruction, canonical instruction data, and PDA re-derivation checks.
typescript/packages/mpp/src/server/session/on-chain.ts Adds verifyChannelAccountState, verifySignatureOnlyOpenTransaction, canonical account list validation, and encoding="base64" for all transaction fetches; assertSignatureSucceeded now throws instead of falling back to 0n.
rust/crates/kit/src/mpp/server/session.rs Adds comprehensive on-chain state binding to process_open and process_topup, including grace_period, distribution_hash, and settlement watermark checks; adds durability guard require_production_session_safety.
typescript/packages/mpp/src/server/Session.ts Enforces durable shared store requirement on mainnet, adds canonical assertSignatureSucceeded with strict slot validation, and gates server-submitted openSignature correctly.
python/src/solana_pay_kit/protocols/mpp/server/session.py Binds open_signature only for server-submitted opens, adds state-aware verifier dispatch, and enforces durability guard; consistent with Go and TypeScript behavior.
go/protocols/mpp/server/session_store.go Adds Salt, OpenSignature, SettlementWire, and settlement lease fields to ChannelState; introduces SessionStoreDurability interface and MemoryChannelStore durability declaration.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Server
    participant RPC as Solana RPC
    participant Store as Session Store

    Note over Server: process_open (Push/Pull)
    Client->>Server: OpenPayload (signature, transaction?)
    Server->>Server: require_production_session_safety()
    Server->>RPC: getSignatureStatuses(signature)
    RPC-->>Server: confirmed slot
    alt has transaction wire
        Server->>Server: verify_open_tx (decode, canonical re-encode, PDA re-derive)
    else signature-only open
        Server->>RPC: "getTransaction(sig, encoding=base64)"
        RPC-->>Server: wire bytes
        Server->>Server: verify_open_tx (structural)
    end
    Server->>RPC: "getAccountInfo(channelPDA, minContextSlot=confirmedSlot)"
    RPC-->>Server: Channel account bytes
    Server->>Server: fetchAndBindChannel (discriminator, mint, payee, operator, grace_period, distribution_hash, PDA re-derive)
    Server->>Store: put ChannelState (bound deposit, payer, salt, open_slot)
    Store-->>Server: OK
    Server-->>Client: Receipt

    Note over Server: process_topup
    Client->>Server: TopUpPayload (signature, newDeposit)
    Server->>Store: get ChannelState
    Store-->>Server: existing state
    Server->>RPC: getSignatureStatuses(signature)
    RPC-->>Server: confirmed slot
    Server->>RPC: "getTransaction(sig, encoding=base64)"
    RPC-->>Server: wire bytes
    Server->>Server: verify exactly 1 topUp instruction for channel
    Server->>RPC: "getAccountInfo(channelPDA, minContextSlot=confirmedSlot)"
    RPC-->>Server: Channel account bytes
    Server->>Server: bind channel state
    Server->>Store: update_channel (raise deposit)
    Store-->>Server: new ChannelState
    Server-->>Client: OK
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Server
    participant RPC as Solana RPC
    participant Store as Session Store

    Note over Server: process_open (Push/Pull)
    Client->>Server: OpenPayload (signature, transaction?)
    Server->>Server: require_production_session_safety()
    Server->>RPC: getSignatureStatuses(signature)
    RPC-->>Server: confirmed slot
    alt has transaction wire
        Server->>Server: verify_open_tx (decode, canonical re-encode, PDA re-derive)
    else signature-only open
        Server->>RPC: "getTransaction(sig, encoding=base64)"
        RPC-->>Server: wire bytes
        Server->>Server: verify_open_tx (structural)
    end
    Server->>RPC: "getAccountInfo(channelPDA, minContextSlot=confirmedSlot)"
    RPC-->>Server: Channel account bytes
    Server->>Server: fetchAndBindChannel (discriminator, mint, payee, operator, grace_period, distribution_hash, PDA re-derive)
    Server->>Store: put ChannelState (bound deposit, payer, salt, open_slot)
    Store-->>Server: OK
    Server-->>Client: Receipt

    Note over Server: process_topup
    Client->>Server: TopUpPayload (signature, newDeposit)
    Server->>Store: get ChannelState
    Store-->>Server: existing state
    Server->>RPC: getSignatureStatuses(signature)
    RPC-->>Server: confirmed slot
    Server->>RPC: "getTransaction(sig, encoding=base64)"
    RPC-->>Server: wire bytes
    Server->>Server: verify exactly 1 topUp instruction for channel
    Server->>RPC: "getAccountInfo(channelPDA, minContextSlot=confirmedSlot)"
    RPC-->>Server: Channel account bytes
    Server->>Server: bind channel state
    Server->>Store: update_channel (raise deposit)
    Store-->>Server: new ChannelState
    Server-->>Client: OK
Loading

Reviews (9): Last reviewed commit: "fix(go): accept the unsigned fee-payer s..." | Re-trigger Greptile

Comment thread rust/crates/kit/src/mpp/server/session.rs
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires confirmed on-chain Channel account state into every MPP session open and top-up across TypeScript, Go, Rust, and Python. Confirmation alone no longer satisfies the security invariant — each SDK now fetches the Channel account at the confirmed slot, validates all PDA seeds (payer, payee, mint, authorized signer, salt, open slot, grace period, distribution hash), and overwrites the session's deposited amount with the authoritative on-chain value.

  • Durability gate: process-local stores are rejected outside localnet across all four SDKs; off-localnet construction fails unless the store declares durable-shared, preventing silent in-memory session state in production.
  • Open binding: fetchAndBindChannelAccount / verifyChannelAccountState replace the old confirmTransactionSignature-only gate for push and transaction-backed opens; server-broadcast opens persist openSignature so retries re-confirm the original broadcast rather than a freshly signed placeholder.
  • Top-up binding: VerifyTopUpStateTx / verify_top_up_state_tx confirm the signature, decode the instruction to verify the funding delta, then re-read the Channel account to compare authorized_signer, payer, and deposit against stored state before raising the local balance.
  • Confirmed-commitment fix: TypeScript, Go, Python, and Rust now require confirmationStatus to be "confirmed" or "finalized" explicitly; Python previously accepted a missing status field as equivalent to confirmed.

Confidence Score: 4/5

The change substantially hardens the session open/top-up paths by binding persisted state to confirmed on-chain Channel accounts, but one cross-SDK validation gap remains: Go accepts N≥1 matching topUp instructions and sums their amounts, while TypeScript and Python reject anything other than exactly one.

The four SDKs disagree on how many topUp instructions a single transaction may carry for a given channel — Go allows multiple and sums them, TypeScript and Python require exactly one. This is a live behavioural difference in a security boundary: a transaction crafted to satisfy Go but rejected by TypeScript/Python (or vice versa) produces inconsistent outcomes in mixed-SDK deployments.

go/protocols/mpp/server/session_onchain.go (NewTopUpStateTxVerifier instruction-count logic) and python/src/solana_pay_kit/protocols/mpp/server/session.py (open_signature stored for all opens).

Important Files Changed

Filename Overview
go/protocols/mpp/server/session_onchain.go Core Go account-binding layer: adds fetchAndBindChannelAccount, NewTopUpStateTxVerifier, confirmedTransactionSlot; allows 1+ top-up instructions per channel (Go allows N≥1, TS/Python require exactly 1)
go/protocols/mpp/server/session_method.go Method layer correctly wires fetchAndBindChannelAccount after every payment-channel open (transaction-backed and push), persists salt/openSignature, and drops the old confirmTransactionSignature-only top-up path in favour of VerifyTopUpStateTx
typescript/packages/mpp/src/server/session/on-chain.ts Adds verifyChannelAccountState (full PDA-seed binding), verifyTopUpTransaction (requires exactly 1 top-up instruction), and GetAccountInfoRpc/TopUpTransactionRpc interfaces; confirmationStatus gate tightened to require confirmed/finalized
typescript/packages/mpp/src/server/Session.ts handleOpen and handleTopUp now call verifyChannelAccountState after confirming the signature, binding deposit/payer/openSlot/salt to on-chain truth; assertSignatureSucceeded upgraded to return slot; requireProductionSessionSafety enforced at session construction
python/src/solana_pay_kit/protocols/mpp/server/session.py Adds _require_production_session_safety, verify_top_up_state_tx seam, and VerifyTopUpStateTx type; open_signature is stored unconditionally for any open with a non-empty signature, diverging from Go/TS which only set it for server-submitted opens
python/src/solana_pay_kit/protocols/mpp/server/session_onchain.py Adds fetch_and_bind_channel_account, new_top_up_state_tx_verifier, _verify_confirmed_top_up (requires exactly 1 matching instruction); confirm_transaction_signature upgraded to return slot and now requires explicit confirmed/finalized status
rust/crates/kit/src/mpp/server/session.rs Full account-binding loop added to process_open and process_topup; require_production_session_safety added to all state operations; allow_unsafe_ephemeral_store_off_localnet escape hatch added; salt/open_signature persisted to ChannelState
typescript/packages/pay-kit/src/adapters/mpp-session.ts createSessionEngine now requires config.mpp.sessionStore outside localnet and validates its durability marker; throws ConfigurationError for unmarked stores

Comments Outside Diff (3)

  1. go/protocols/mpp/server/session_onchain.go, line 663-668 (link)

    P1 Go allows N≥1 matching top-up instructions; TypeScript and Python require exactly 1

    Go sums all matching topUp instructions for the channel and only rejects when matched == 0. TypeScript (verifyTopUpTransaction) throws "expected exactly one top-up, found N" for any count other than 1, and Python's _verify_confirmed_top_up applies the same constraint. A transaction carrying two topUp instructions for the same channel is therefore accepted by Go (if the sum matches the claimed delta) but rejected by TypeScript and Python servers handling the same payload. This creates a cross-SDK gap in a security-critical validation path — a scenario that should produce a deterministic result across all implementations.

  2. python/src/solana_pay_kit/protocols/mpp/server/session.py, line 362-378 (link)

    P2 open_signature written for every open that carries a non-empty signature

    open_signature=payload.signature or None is evaluated unconditionally in fresh_state, so client-submitted (and push-with-no-transaction) opens will store the client's transaction signature in the field. Go and TypeScript only populate this field for server-submitted opens (var openSignature string defaults to empty in Go; TypeScript gates it with openTxSubmitter === 'server'). The field itself is only checked in the server-submitted retry branch, so there is no direct runtime failure today. However, if a client-submitted channel's ID were somehow later retried via the server-submitted path (e.g. a misconfigured multi-SDK deployment), the guard existing.open_signature is None would silently pass and the client signature would be used as the authoritative server-broadcast signature.

  3. typescript/packages/mpp/src/server/Session.ts, line 1290-1307 (link)

    P2 assertSignatureSucceeded falls back to 0n when context.slot is absent

    response.context?.slot ?? 0n returns 0n whenever the RPC response omits the context object. That value is then passed as minContextSlot to verifyChannelAccountState, which will forward it to getAccountInfo. A minContextSlot of 0 is effectively no constraint — any cached slot on the RPC node satisfies it — removing the freshness guarantee that ties the account fetch to the confirmed transaction. The same pattern exists in Python (return slot if isinstance(slot, int) and slot >= 0 else 0). Real Solana RPC nodes always populate context, but the silent fallback means misbehaving or mock nodes bypass the invariant without any error.

Reviews (2): Last reviewed commit: "fix(mpp): fetch confirmed top-up transac..." | Re-trigger Greptile

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please re-review. The session settlement path now uses a durable transactional outbox, retry-safe exact-wire rebroadcast, lease recovery, expiry retirement, and construction-time RPC capability validation; focused Go/Python/Rust/TypeScript verification is green.

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please re-review the final static-analysis head. The only follow-up changes simplify the always-present verified-open value and apply repository-mandated TypeScript key/import ordering; Go lint/full focused tests and TypeScript 568 tests/lint/format/typecheck are green.

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please review

Read the on-chain Channel account at the confirmed slot and bind session
authoritative state (deposit, splits, grace period) to it. Session state is
never advanced or trusted for an unconfirmed or economically mismatched
channel. No sibling PR touches the Rust session server; this layer is unique
to #239 and applies cleanly on the settled base.
Add #239's unique fail-closed binding: a session open/top-up is rejected
unless the on-chain Channel account (fetched via getAccountInfo at the
confirmed slot) confirms the claimed deposit, mint, payee, authorized
signer, grace period and distribution hash, and a signature-only open is
bound to its canonical broadcast transaction.

- server/session/on-chain.ts: verifyChannelAccountState,
  verifySignatureOnlyOpenTransaction, GetAccountInfoRpc, TopUpTransactionRpc.
- server/session/wire-tx.ts + store.ts: openSignature/salt/settlement fields
  carried on ChannelState for the binding.
- server/Session.ts: wire the verify calls into open/top-up/routes; export
  SettlementRpc.
- tests: session-state-binding, session-signature-only-open, plus the #239
  additions to session-on-chain / session-server-on-chain / session-store.

Dedup against stacked siblings #237/#238 (already in the base): keep their
session-store durability guard (resolveSessionStore + allowUnsafeMemoryStore)
and replay-store policy; drop #239's independent re-implementation of the
same concern (requireProductionSessionSafety + allowUnsafeEphemeralStoreOffLocalnet
+ the adapter wiring), which conflicted with those siblings' regression tests.
Superseded session-store-durability assertions were removed from the #239
test files; the mainnet fail-closed control is unchanged (stronger via #237).
Reconcile #239's on-chain Channel-account state binding onto the base's
solana-go v2 migration and hardened open/top-up verification (#214). Adds the
authoritative state-binding layer that no sibling or the base delivers:
fetchAndBindChannelAccount, NewOpenStateTxVerifier, bindConfirmedOpenChannel,
validateBoundOpenChannel, NewTopUpStateTxVerifier and confirmedTransactionSlot.
Session open/top-up state is bound to the on-chain Channel account read at the
confirmed slot; an unconfirmed or economically mismatched channel is rejected
(fail closed). Base store-safety (env-gated) is preserved; the config-field
guard reconciles to honor the same PAY_KIT_ALLOW_INMEMORY_REPLAY_STORE opt-in.

Ports #239's v1 solana-go usage to v2 and keeps base's structural verifiers +
concurrency/replay hardening; drops #239's duplicate top-up-fence and legacy
verifier redefinitions superseded by the base.
Add PR #239's unique behavior on top of sibling #228: the state-aware open
verifier that fetches the on-chain Channel account at the confirmed slot and
binds deposit, splits, grace period and PDA seeds to on-chain reality, so a
payload's claimed economics are never persisted as facts.

- session_onchain: BoundChannel, fetch_and_bind_channel_account,
  new_open_state_tx_verifier (+ signature-only confirm/bind helpers,
  _session_distribution_hash, _expected_session_grace_period). The structural
  open path reuses #228's verify_open_tx via the recipients contract.
- confirm_transaction_signature now returns the confirmed slot and rejects a
  present-but-malformed slot, so the account read is pinned with
  min_context_slot; a status that omits the slot still confirms.
- session: SessionConfig.verify_open_state_tx / open_tx_submitter seams,
  VerifiedOpenFacts, and process_open fail-closed: off localnet a
  payment-channel open requires the state verifier and persists only
  account-derived facts.
- session_method: new_session installs the state verifier off localnet;
  _handle_open propagates the challenge recentSlot for signature-only push.
- session_store: ChannelState.salt / open_signature round-trip.
- rpc: SolanaRpc.get_account_info min_context_slot + typed get_transaction.
- tests: add test_session_state_binding.py (adapted to #228's
  ProductionChannelStore policy, which is kept as-is).

#228's structural open, top-up fence, channel-store policy and replay code are
kept unchanged; their tests continue to pass.
Give the shared test operator a real signing key (sessionTestOperatorKey backs
sessionTestRecipient) and co-sign the rentPayer slot so signature-only open
fixtures reproduce a fully signed confirmed transaction. Base #214 verifies open
signatures cryptographically, so the prior dummy-signature fixtures no longer
pass. Also restores base's env-gated nil-store default (a merge artifact had
re-added an unconditional off-localnet rejection that ignored the opt-in).
Base #214 hardened open verification (real wire-bound signatures, no
placeholder acceptance) and the session store policy became env-gated.
Update the stale Go test fixtures/assertions to the reconciled behavior:

- flip TestNewOpenTxVerifierKeepsStructuralPlaceholderAcceptance to
  TestNewOpenTxVerifierRejectsPlaceholderSignature: the seam now rejects the
  pending placeholder ("must be a real transaction signature")
- assert the real confirmed signature and derived PDA in the signature-only
  open test rather than the dummy reference the caller supplied
- thread the derived channel PDA into the top-up test and reflect the
  post-top-up deposit in the authoritative on-chain account
- match the reconciled off-localnet store-policy message, and enforce the
  unmarked-store rejection at the runtime safety gate (where it now lives)
  rather than at construction
- rename the idle-close no-signer test and assert the no-op: closeOnIdle bails
  before touching the channel when no merchant signer is configured

Test-only; no production source changed.
… path

prepareOpenTx (SubmitOpenTx, the gasless server-submit flow) verified the
open with allowUnsignedFeePayer=false, rejecting the placeholder fee-payer
signature before the co-sign step the same function performs. The Go
client signs only as the channel payer and leaves the operator slot for
the server, so the server rejected its own client's open. Use the same
verifyOpenTx(..., true) pre-verification handleOpen already uses: the
payer signature is still verified and the post-co-sign zero-signature
guard keeps broadcast fail-closed.
@EfeDurmaz16
EfeDurmaz16 force-pushed the fix/mpp-session-state-hardening branch from 7ecf8e1 to 7012008 Compare July 14, 2026 00:13
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptileai review

…resh test doubles

confirm_transaction_signature returns int|None (a confirmed status may omit the
slot), so _fetch_and_verify_signature_only_open's tuple[int, ...] annotation was
unsound; the slot flows into min_context_slot (int|None), so widen the return.
Refresh the _Rpc/_OpenConfig test doubles to the current RpcClient
(search_transaction_history kwarg) and OpenVerifierConfig (settlement_window)
protocols so pyright's structural check passes (13 errors -> 0).
…aths

Add rejection-branch tests for validateBoundOpenChannel, resolveSessionTokenProgram,
NewOpenStateTxVerifier/verifySignatureOnlyOpen, fetchAndBindChannelAccount, and the
top-up verifiers (each asserts the concrete fail-closed error, not just coverage).
Raises Go aggregate coverage 90.3% -> 91.1%, back over the 91.0 floor.
…ard for radar

Prettier the mpp server index/session store re-exports. Mark the localnet-only
MemoryChannelStore fallback nosemgrep: enforce_channel_store_policy above already
rejects an absent/in-memory store off-localnet without the opt-in, which the radar
rule's inline-guard pattern cannot see.
…ture

The epoch-addressed channel PDA seeds include open_slot, so the session
challenge now pre-fetches recentSlot alongside recentBlockhash and the open
builder fails closed without it. The defect-verifier fixture predates that
contract and crashed at construction instead of reaching its rejection
assertions; supply the challenge slot so all five value-binding rejections
(and the accept controls) exercise their intended paths.
semgrep only honors a nosemgrep comment on the match line or the line
immediately before it; the wrapped two-line comment left the marker one
line too far and the blocking radar scan still flagged the guarded
localnet-only MemoryChannelStore fallback.
EfeDurmaz16 added a commit that referenced this pull request Jul 14, 2026
… union

Order the mpp-session leaf (#239) ahead of the python leaf (#228) so their
overlapping edits to the shared ci.yml + boot-policy.test.ts interleave without
a phantom 3-way conflict (both are ultimately reconciled by the harness meta
leaf, which owns those paths and merges last). Add a pure-union assertion after
the merges: every non-merge commit the rehearsal introduces over base must be
contained in some leaf head, else it is a hand-authored patch on #240 and the
script refuses to push. This makes 'green by construction' mechanically checked,
not asserted.
…into fix/mpp-session-state-hardening

# Conflicts:
#	.github/workflows/ci.yml
#	go/protocols/mpp/server/session.go
#	go/protocols/mpp/server/session_method.go
#	go/protocols/mpp/server/session_method_test.go
#	go/protocols/mpp/server/session_onchain.go
#	go/protocols/mpp/server/session_onchain_test.go
#	typescript/packages/mpp/src/__tests__/session-server.test.ts
#	typescript/packages/mpp/src/server/Session.ts
#	typescript/packages/mpp/src/server/index.ts
#	typescript/packages/mpp/src/server/session/store.ts
#	typescript/packages/pay-kit/src/__tests__/mpp-session.test.ts
This leaf's session close decodes the signed wire to bind the broadcast
signature before submitting, so the fixture's placeholder string no longer
satisfies the contract. Bring forward the reconciled fixture: it compiles the
composed instructions into a real signed transaction and asserts the reported
signature is derived from the wire, never trusted from the RPC response. The
gate travels with the hardening that created its need.
… into fix/mpp-session-state-hardening

# Conflicts:
#	go/protocols/mpp/server/session_server_test.go
#	go/protocols/mpp/server/session_settlement_durability_test.go
#	go/protocols/mpp/server/session_state_binding_test.go
#	harness/test/value-binding-verify.test.ts
#	python/src/solana_pay_kit/protocols/mpp/server/session.py
#	python/src/solana_pay_kit/protocols/mpp/server/session_method.py
#	python/src/solana_pay_kit/protocols/mpp/server/session_onchain.py
#	python/src/solana_pay_kit/protocols/mpp/server/session_store.py
#	python/tests/test_session_method.py
#	python/tests/test_session_onchain.py
#	python/tests/test_session_server.py
#	python/tests/test_session_settlement.py
#	python/tests/test_session_state_binding.py
#	rust/crates/kit/src/mpp/server/session.rs
#	typescript/packages/mpp/src/__tests__/session-state-binding.test.ts
…tions

The playground example still passed the removed
allow_unsafe_ephemeral_store_off_localnet escape hatch to SessionOptions, so
importing it (and collecting tests/test_playground_api.py) fails with a
TypeError against this leaf's hardened session surface. Bring the example to
the reconciled version, which constructs the session method without the
ephemeral-store escape. Full python suite: 1518 passed, 1 skipped.
@EfeDurmaz16
EfeDurmaz16 changed the base branch from split/pr216-ci-harness-mega to fix/python-security-hardening July 15, 2026 11:17
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.

1 participant