Add opt-in Observer Protocol pre-sign policy check (fail-closed) for Liquid sends#120
Draft
BTCBoyd wants to merge 4 commits into
Draft
Add opt-in Observer Protocol pre-sign policy check (fail-closed) for Liquid sends#120BTCBoyd wants to merge 4 commits into
BTCBoyd wants to merge 4 commits into
Conversation
…Liquid sends When enabled via OP_POLICY_ENABLED, WalletManager.send() submits the exact unsigned transaction to the Observer Protocol policy engine and signs only on a verified "allow". Deny, non-2xx, an unreachable engine, or an unverifiable/expired credential all fail closed: the wallet does not sign. Inert when the flag is unset, so send() behaves exactly as before by default. - src/aqua/op_policy.py: evaluate-or-raise module + canonical_bytes() PSET extraction - src/aqua/op_verify.py: self-contained PEC verifier (eddsa-jcs-2022, did:web #key-3); float-leaf guard fails closed outside the proven JCS integer/string range - src/aqua/wallet.py: pre-sign block in send() with a same-PSET identity guard - README: enable/disable, env vars, fail-closed contract, verification scope, rail coverage - .gitignore: op-artifacts/ (default deny-credential output dir) Zero new dependencies (cryptography is already required for mnemonic encryption).
…y relabel The engine now selects the spending cap by asset_id and enforces per-asset, so the wallet passes the TRUE unit of the asset being sent (its ticker, resolved from asset_id via Aqua's registry) instead of relabelling the unit to match the cap currency. That relabel was what let an uncapped asset ride the wrong cap; a send whose asset has no cap is now refused (engine: no-cap-for-asset), and an asset unknown to the registry is refused (unknown-asset). - op_policy.py: remove _resolve_unit / _delegation_cap_unit relabel; pass caller-supplied unit through (OP_POLICY_UNIT still overrides for testing) - wallet.py: resolve the asset ticker via lookup_asset(asset_id, network) and pass it as humanReadable.unit; L-BTC (no asset_id) -> "L-BTC" L-BTC sends are unchanged. Validated end-to-end against the real policy engine: L-BTC allow, USDt within-cap allow, USDt over-cap deny, USDt no-cap deny (no-cap-for-asset), unknown-asset deny.
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.
Add opt-in Observer Protocol pre-sign policy check (fail-closed) for Liquid sends
What this does
Adds an opt-in, fail-closed policy check to the Liquid send path. When enabled via a
single env var (
OP_POLICY_ENABLED), the wallet submits the exact unsigned transactionto the Observer Protocol policy engine and only signs on a verified
allowdecision.Deny, non-2xx, an unreachable engine, or a credential whose signature won't verify all
fail closed — the wallet does not sign.
When the flag is off, this code is inert and
send()behaves exactly as it does today.It's additive, env-gated, and adds no wallet dependencies beyond the one noted below.
The path
The thing that is genuinely real here — and can't be faked — is that a real send is gated
by a real signed credential from the real engine, and the wallet verifies that signature
itself before trusting the decision.
Rail coverage
selected by
asset_id(the cryptographic ground truth of what's moving), not a currencylabel. Validated live against production with the returned credential's signature
verified: L-BTC within cap →
allow, over →deny; USDt within its own cap →allow,over →
deny; a USDt send with no USDt cap →deny(no-cap-for-asset, not wavedthrough the L-BTC cap); an asset unknown to the engine registry →
deny(
unknown-asset). The delegation carries an optionalper_rail.liquid.per_assetmap(context v2); native L-BTC keeps the rail-level cap, so every already-issued delegation
is unchanged.
(
lightning.py): the submarine-swap lockup is an L-BTCwallet_manager.send()routedthrough this same path, so it's evaluated as a Liquid send (counterparty = Boltz swap
address).
bitcoin.pybuilds a BDK PSBT andsigns directly, bypassing this hook. Extending the same pre-sign pattern to that path is
a clean follow-up, called out here rather than left silent.
Verification scope — what the wallet checks, and what it doesn't yet
This is the honest core of the PR, stated plainly:
The wallet resolves
did:web:observerprotocol.org#key-3from the published DID documentand verifies the
eddsa-jcs-2022proof itself. It trusts a signed decision, not thetransport. If the signature doesn't verify, it fails closed.
revocation status, and validity window. The hosted
/policy/evaluateenforces themandate's constraints against the delegation as-provided, but it does not authenticate
the delegation and does not reject an expired one (confirmed live: an out-of-window
delegation still evaluates to
allow), and this PR doesn't check those either. Fullcaller-side delegation verification (signature + revocation +
validFrom/validUntil)is the explicit next step. We'd rather name this than ship it silently unhandled under a
"verify everything" banner.
Fail-closed still holds unconditionally for engine reachability, HTTP status, decision
value, and PEC-signature validity.
Configuration (env-driven)
OP_POLICY_ENABLED1/true/yesto enable.OP_DELEGATION_PATHOP_SIDECAR_URLhttps://api.observerprotocol.org/policy/evaluateOP_POLICY_TIMEOUT10OP_VERIFY_PECtrueOP_POLICY_UNITlookup_asset(asset_id)); set this only to force a unit for testing.OP_DENY_ARTIFACT_DIR./op-artifactsFail-closed contract
The wallet signs only on a verified
allow. Every one of these refuses to sign:validUntilin the past) → refusedeny→ persist the signed deny artifact, then refusePlus a same-PSET identity guard: the transaction handed to the signer must be the exact
one that was evaluated.
Dependency note
Zero new dependencies. PEC signature verification needs Ed25519 verify; the vendored
verifier (
src/aqua/op_verify.py, self-contained) usescryptography, which is already adirect dependency of Aqua (
pyproject.toml:cryptography>=42.0.0, used for mnemonicencryption). Nothing is added to the dependency set.
Explicitly out of scope (deferred)
Native BTC/BDK path; a pluggable
pre_sign_hookregistry; surfacing config throughconfig.json(the env gate keeps it honestly "opt-in demo→beta"); delegation transportpolish; seeding the Liquid attestation graph; full caller-side delegation
signature/revocation verification (named above); OP Crossrail (adapters,
crossRailBudget, the cross-rail ledger — orthogonal to this hosted-endpoint path and notwired into it); the not-yet-enforced advisory rule families; install ergonomics.
Forward-compat: the hook tolerates unknown extra credential fields, so future additive
fields (e.g. a Crossrail budget) won't break this integration.
Files
src/aqua/op_policy.py— evaluate-or-raise module (replaces the demoop_policy_demo.py, which is removed). Submits the send with the caller-supplied trueasset unit (no cap-currency relabel — the engine selects the cap by
asset_id), verifiesthe returned PEC's signature, persists deny artifacts, logs to stderr, and carries the
canonical_bytes()PSET-extraction helper.src/aqua/op_verify.py— vendored, self-contained PEC verifier (eddsa-jcs-2022 /#key-3), with a float-leaf guard that fail-closes rather than trust the JCSapproximation outside its proven integer/string range.
src/aqua/wallet.py— the pre-sign block inWalletManager.send()plus the import.README— the Observer Protocol section (enable/disable, env vars, fail-closed contract,verification scope, rail coverage).
Base
Built as a single additive feature on top of current
develop(==mainat the time ofwriting), targeting
developper the repo's PR convention.