@hazbase/simplicity 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,21 @@ It is intentionally narrower than a full market stack. It does **not** try to be
12
12
 
13
13
  This SDK is designed to help Node developers get productive quickly, but it is still opinionated, early-stage, and best suited today to permissioned settlement pilots on Liquid.
14
14
 
15
+ ## Version 0.4.7
16
+
17
+ This release extends strict-policy positions from ordinary transfers to a
18
+ complete controlled lifecycle:
19
+
20
+ - ordinary transfers remain restricted to the approved holder whitelist;
21
+ - redemptions return the position to a bound vault under an authority signature;
22
+ - governance can rotate the verifier to a reviewed successor without moving the
23
+ protected asset; and
24
+ - browser wallets can provide their own audited crypto implementation before
25
+ any strict-policy helper loads, avoiding extension CSP violations.
26
+
27
+ For policy-locked redemptions, Liquid x402 now binds a wallet-created holder
28
+ signature for the locally recomputed transaction hash into the payment payload.
29
+
15
30
  ## What You Can Build
16
31
 
17
32
  With the current public SDK you can build and test:
@@ -22,6 +37,7 @@ With the current public SDK you can build and test:
22
37
  - receivable repayment-first funding / partial repayment / closing flows
23
38
  - RWA delivery-versus-payment flows over Liquid PSETs and Liquid x402
24
39
  - atomic Liquid DvP proposals where service delivery and buyer payment settle in one PSET
40
+ - canonical, signed policy snapshots for strict DAMP-compatible wallet validation
25
41
  - evidence, trust summary, lineage, and finality exports
26
42
 
27
43
  ## Public Architecture
@@ -34,6 +50,8 @@ The public SDK is organized into domain clients and lower-level payment helpers:
34
50
  - `sdk.receivables`: repayment-first receivable business layer
35
51
  - `sdk.rwaDvp`: RWA purchase terms, payment requirements, claim descriptors, and evidence
36
52
  - `sdk.payments.x402`: Liquid x402 assets, PSET payment helpers, verification, and settlement
53
+ - root exports such as `prepareStrictPolicySnapshot(...)` and
54
+ `verifyStrictPolicySnapshot(...)`: canonical policy snapshot and authority-proof validation
37
55
 
38
56
  A useful mental model is:
39
57
  - `sdk.outputBinding` + `sdk.policies` provide the shared settlement kernel
@@ -82,6 +100,110 @@ npx simplicity-cli --help
82
100
 
83
101
  For richer walkthroughs and sample JSON / `.simf` files, use [`docs/definitions/README.md`](./docs/definitions/README.md).
84
102
 
103
+ ## Strict Policy Snapshots And Covenants
104
+
105
+ The root-level strict policy helpers provide the strict-asset validation and
106
+ covenant layer. They canonicalize and verify an approved snapshot, derive the
107
+ canonical holder output `CU(X)`, build fixed-depth owner whitelist proofs, and
108
+ render or compile matching holder and verifier covenants.
109
+
110
+ ```ts
111
+ import {
112
+ buildStrictPolicyWhitelist,
113
+ deriveStrictPolicyHolderPosition,
114
+ inspectStrictPolicyTransactionWithProofs,
115
+ prepareStrictPolicySnapshot,
116
+ verifyStrictPolicySnapshot,
117
+ } from "@hazbase/simplicity";
118
+
119
+ const prepared = prepareStrictPolicySnapshot({ payload, authority });
120
+ console.log(prepared.snapshotHash, prepared.approvalMessageHash);
121
+
122
+ await verifyStrictPolicySnapshot({
123
+ ...prepared,
124
+ approvalProof,
125
+ approvalProofHash,
126
+ });
127
+
128
+ const whitelist = buildStrictPolicyWhitelist([recipientXonly, changeXonly]);
129
+ if (whitelist.root !== prepared.payload.whitelistRoot) {
130
+ throw new Error("Whitelist does not match the active policy snapshot");
131
+ }
132
+ const recipientPosition = await deriveStrictPolicyHolderPosition({
133
+ holderProgramCmr,
134
+ numsInternalKey,
135
+ holderXonly: recipientXonly,
136
+ });
137
+ const proofFor = (ownerXonly: string) => {
138
+ const entry = whitelist.entries.find((candidate) => candidate.ownerXonly === ownerXonly);
139
+ if (!entry) throw new Error("Owner is not in the active whitelist");
140
+ return entry.proof;
141
+ };
142
+ const strictOutputAuthorizations = [
143
+ { outputIndex: 1, ownerXonly: recipientXonly, proof: proofFor(recipientXonly) },
144
+ { outputIndex: 2, ownerXonly: changeXonly, proof: proofFor(changeXonly) },
145
+ ];
146
+
147
+ const inspection = await inspectStrictPolicyTransactionWithProofs(decodedTransaction, {
148
+ operation: "transfer",
149
+ snapshot: prepared.payload,
150
+ feeAssetId,
151
+ maxFeeAmountAtomic: "500",
152
+ holderProgramCmr,
153
+ holderNumsInternalKey: numsInternalKey,
154
+ strictOutputAuthorizations,
155
+ });
156
+ console.log(recipientPosition.scriptPubKey, inspection.summaryHash);
157
+ ```
158
+
159
+ The current public schema deliberately supports `issuer_single` only. Its
160
+ authority fields are typed so a separately specified quorum or multisig scheme
161
+ can be added later without treating unknown proof types as valid.
162
+ `strict-holder-input-v1.simf` proves the owner-bound `CU(X)` and holder
163
+ signature. `strict-verifier-ordinary-v1.simf` fixes the verifier at input and
164
+ output zero and exposes three mutually exclusive paths:
165
+
166
+ - ordinary transfer: every strict output must be a canonical `CU(X)` for a key
167
+ proven under the active whitelist root;
168
+ - redemption: every strict output must return to the configured vault and the
169
+ configured authority signs the complete transaction; and
170
+ - governance: the authority signs a verifier-only transition to a reviewed
171
+ successor contract, without moving any strict asset.
172
+
173
+ The pilot bound is 16 inputs, 8 outputs, and 256 whitelist owners; larger
174
+ transactions fail closed. The verifier parameters now require both
175
+ `redemptionScriptHash` and `authorityXonly`, so callers must bind the vault and
176
+ the authority explicitly when compiling a new verifier.
177
+
178
+ `inspectStrictPolicyTransactionWithProofs(...)` is the recommended wallet API.
179
+ It validates every strict output by index, verifies its owner proof, derives the
180
+ destination script locally, and then checks verifier continuity, asset
181
+ conservation, issuance absence, and the fee ceiling. The lower-level
182
+ `inspectStrictPolicyTransaction(...)` remains available for adapters that have
183
+ already completed that proof-to-script derivation.
184
+
185
+ `inspectStrictPolicyPsetForSigning(...)` is the signing boundary for a complete
186
+ PSET. Supply a local lossless decoder through `decodePset`; the SDK then binds
187
+ the canonical PSET bytes to the decoded transaction, validates the snapshot,
188
+ CMR, verifier, every strict output and fee limit, and returns only the locally
189
+ recomputed Simplicity `sig_all_hash`. A service-provided hash is accepted only
190
+ as a comparison hint and causes rejection when it differs.
191
+
192
+ The settlement repository includes
193
+ `hazbase-simplicity-inspect-pset`, a native reference decoder built on the
194
+ official `hal-simplicity` sighash implementation. A browser wallet still needs
195
+ to connect an equivalent lossless WASM decoder to the SDK callback.
196
+ The SDK supplies the canonical schema, compiler inputs and x402 holder-signature
197
+ payload binding. A wallet must still decode the complete PSET locally, derive
198
+ the exact `sig_all_hash`, construct the branch witness, and verify the active
199
+ snapshot before signing. Transaction-level regtest/testnet vectors remain
200
+ release gates; these templates and checks alone are not a production activation.
201
+
202
+ The pinned public vector is
203
+ [`strict-policy-snapshot-golden-v1.json`](./docs/definitions/strict-policy-snapshot-golden-v1.json).
204
+ The holder-output and whitelist vector is
205
+ [`strict-cu-whitelist-golden-v1.json`](./docs/definitions/strict-cu-whitelist-golden-v1.json).
206
+
85
207
  ## Domain Overview
86
208
 
87
209
  ### Policy
@@ -181,8 +303,11 @@ Liquid x402 request with `extra.redemptionSource.type:
181
303
  `buildLiquidPolicyLockedRedemptionPaymentFromProposal(...)` when a service has
182
304
  already prepared the Simplicity policy-spend PSET proposal. The helper binds the
183
305
  custom RWA asset, policy position, vault output, expiry, and summary hash into
184
- the `X-PAYMENT` payload, and rejects proposals that still require holder-side
185
- Simplicity witness construction.
306
+ the `X-PAYMENT` payload. When the proposal requires a holder signature, supply
307
+ the locally derived `holderSignature` for the exact `sig_all_hash`; the helper
308
+ then carries the request and signature together without accepting a
309
+ service-provided signing hash as authoritative. It still rejects proposals that
310
+ require the wallet to construct an unspecified Simplicity witness.
186
311
 
187
312
  Services can prepare that policy-spend proposal with
188
313
  `sdk.policies.inspectTransfer(...)` or `sdk.policies.executeTransfer(...)`.
@@ -6,15 +6,9 @@ exports.schnorrPublicKeyFromPrivkeyHex = schnorrPublicKeyFromPrivkeyHex;
6
6
  exports.schnorrSignHex = schnorrSignHex;
7
7
  exports.schnorrVerifyHex = schnorrVerifyHex;
8
8
  const errors_1 = require("./errors");
9
- // Keep native dynamic import intact under CommonJS output so Node can load the ESM-only noble package.
10
- const nativeImport = new Function("specifier", "return import(specifier);");
11
- let schnorrPromise;
9
+ const strictPolicyCrypto_1 = require("./strictPolicyCrypto");
12
10
  async function loadSchnorr() {
13
- schnorrPromise ??= nativeImport("@noble/curves/secp256k1.js").then((module) => {
14
- const typed = module;
15
- return typed.schnorr;
16
- });
17
- return schnorrPromise;
11
+ return (await (0, strictPolicyCrypto_1.loadStrictPolicyCrypto)()).schnorr;
18
12
  }
19
13
  function assertHex(value, byteLength, code, message) {
20
14
  if (!/^[0-9a-f]+$/i.test(value) || value.length % 2 !== 0) {
@@ -0,0 +1,8 @@
1
+ type Secp256k1Module = typeof import("@noble/curves/secp256k1.js");
2
+ export interface StrictPolicyCryptoProvider {
3
+ secp256k1: Secp256k1Module["secp256k1"];
4
+ schnorr: Secp256k1Module["schnorr"];
5
+ }
6
+ export declare function configureStrictPolicyCrypto(provider: StrictPolicyCryptoProvider): void;
7
+ export declare function loadStrictPolicyCrypto(): Promise<StrictPolicyCryptoProvider>;
8
+ export {};
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configureStrictPolicyCrypto = configureStrictPolicyCrypto;
4
+ exports.loadStrictPolicyCrypto = loadStrictPolicyCrypto;
5
+ // Keep the native import intact under CommonJS output so Node can load noble's
6
+ // ESM package. Construct it only for the Node fallback: Chrome extension CSP
7
+ // rejects `new Function` when a bundled module evaluates, while browser bundles
8
+ // inject the audited implementation through configureStrictPolicyCrypto first.
9
+ function nativeImport(specifier) {
10
+ const importModule = new Function("value", "return import(value);");
11
+ return importModule(specifier);
12
+ }
13
+ let configuredProvider;
14
+ let providerPromise;
15
+ function configureStrictPolicyCrypto(provider) {
16
+ if (!provider?.secp256k1 || !provider?.schnorr) {
17
+ throw new TypeError("Strict policy crypto provider must include secp256k1 and schnorr");
18
+ }
19
+ if (configuredProvider
20
+ && (configuredProvider.secp256k1 !== provider.secp256k1
21
+ || configuredProvider.schnorr !== provider.schnorr)) {
22
+ throw new Error("Strict policy crypto provider is already configured");
23
+ }
24
+ configuredProvider = provider;
25
+ providerPromise = Promise.resolve(provider);
26
+ }
27
+ async function loadStrictPolicyCrypto() {
28
+ providerPromise ??= nativeImport("@noble/curves/secp256k1.js").then((module) => {
29
+ const typed = module;
30
+ return {
31
+ secp256k1: typed.secp256k1,
32
+ schnorr: typed.schnorr,
33
+ };
34
+ });
35
+ return providerPromise;
36
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "schema": "hazbase_strict_policy_cu_whitelist_vector_v1",
3
+ "holderProgramCmr": "72f400043533c5fc9d5e6865b6edb2d22732db3379e54d2643b3c02652c0c87a",
4
+ "numsInternalKey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
5
+ "whitelist": {
6
+ "depth": 8,
7
+ "root": "e3262065c02e0b42c1c63b12ef118202356430ca5b5d272b54e3715f48a7bf23",
8
+ "entries": [
9
+ {
10
+ "ownerXonly": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
11
+ "leafHash": "79943062a19aed81bf32e0b826bb048a94187c52804d3343971090ed4dae8618",
12
+ "proof": [
13
+ { "siblingPosition": "right", "siblingHash": "cf74eec84a4fac9690f00fdef98466da22c68c4931be60986fea89ee99b920cd" },
14
+ { "siblingPosition": "right", "siblingHash": "1c8e82d1dfbc4e86bae912bdf36574b23608a1d099aba732d7ad1f8b98cf3ada" },
15
+ { "siblingPosition": "right", "siblingHash": "44f3a9c851a83b8b22fba37a44813b506e065776053565f486c2cd877ec4a207" },
16
+ { "siblingPosition": "right", "siblingHash": "b32210b7558e6c968279875cd07abccafd6dd29ca62589dd19c686fb32d997bc" },
17
+ { "siblingPosition": "right", "siblingHash": "4a006214dc3467b20dce046d8c0b6614e537b1c8874d0a07ef44d0e411e97a81" },
18
+ { "siblingPosition": "right", "siblingHash": "2b5109b939fd3ba1e538b3f0477960673b24fa9caae2f4c121ca608d44a33f9c" },
19
+ { "siblingPosition": "right", "siblingHash": "15869b4b05b6ac579551352dc91f4c1e06c225a233b7bbda967534b8563a461f" },
20
+ { "siblingPosition": "right", "siblingHash": "696cdfe69a3b0d700ad5c8ce6b14e13e1dbec6ef3283e231d37f6f648e2fbec4" }
21
+ ]
22
+ },
23
+ {
24
+ "ownerXonly": "c6047f9441ed7d6d3045406e95c07cd85a6e326c5a5fe5bdfb8f6935d3a3c96c",
25
+ "leafHash": "cf74eec84a4fac9690f00fdef98466da22c68c4931be60986fea89ee99b920cd",
26
+ "proof": [
27
+ { "siblingPosition": "left", "siblingHash": "79943062a19aed81bf32e0b826bb048a94187c52804d3343971090ed4dae8618" },
28
+ { "siblingPosition": "right", "siblingHash": "1c8e82d1dfbc4e86bae912bdf36574b23608a1d099aba732d7ad1f8b98cf3ada" },
29
+ { "siblingPosition": "right", "siblingHash": "44f3a9c851a83b8b22fba37a44813b506e065776053565f486c2cd877ec4a207" },
30
+ { "siblingPosition": "right", "siblingHash": "b32210b7558e6c968279875cd07abccafd6dd29ca62589dd19c686fb32d997bc" },
31
+ { "siblingPosition": "right", "siblingHash": "4a006214dc3467b20dce046d8c0b6614e537b1c8874d0a07ef44d0e411e97a81" },
32
+ { "siblingPosition": "right", "siblingHash": "2b5109b939fd3ba1e538b3f0477960673b24fa9caae2f4c121ca608d44a33f9c" },
33
+ { "siblingPosition": "right", "siblingHash": "15869b4b05b6ac579551352dc91f4c1e06c225a233b7bbda967534b8563a461f" },
34
+ { "siblingPosition": "right", "siblingHash": "696cdfe69a3b0d700ad5c8ce6b14e13e1dbec6ef3283e231d37f6f648e2fbec4" }
35
+ ]
36
+ }
37
+ ]
38
+ },
39
+ "positions": [
40
+ {
41
+ "holderProgramCmr": "72f400043533c5fc9d5e6865b6edb2d22732db3379e54d2643b3c02652c0c87a",
42
+ "holderTapleafHash": "892f43437d4f9992ee854f4852a4dc533bd70df011f9e8331a1b8e5edb450225",
43
+ "holderXonly": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
44
+ "ownerTapDataHash": "2726c3e2df291658a30abbf145b775c0403192b7bf16a6ee3c6243b00f445bc2",
45
+ "taprootRoot": "35b17c8a4e03bd30b21fc9a27bbbefec267843e6be6c2dc5becc8bfac2a8627c",
46
+ "numsInternalKey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
47
+ "outputKey": "741df228c751ac305ebf6fbd300a7c1e5eddb21415b93782709f6839fafc5931",
48
+ "scriptPubKey": "5120741df228c751ac305ebf6fbd300a7c1e5eddb21415b93782709f6839fafc5931",
49
+ "scriptHash": "14506b881f1c9ef212b7dfb93100b1067f40b876059773c846ffd6a9d9c74080"
50
+ },
51
+ {
52
+ "holderProgramCmr": "72f400043533c5fc9d5e6865b6edb2d22732db3379e54d2643b3c02652c0c87a",
53
+ "holderTapleafHash": "892f43437d4f9992ee854f4852a4dc533bd70df011f9e8331a1b8e5edb450225",
54
+ "holderXonly": "c6047f9441ed7d6d3045406e95c07cd85a6e326c5a5fe5bdfb8f6935d3a3c96c",
55
+ "ownerTapDataHash": "825cae77c977520bf404779e1a66a5f084e1c18bfecf77a9c0ded3d977368e80",
56
+ "taprootRoot": "012c978075295e3f5f32ded5c127050a287b465cdf076efcdb607fd2dfca4c92",
57
+ "numsInternalKey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
58
+ "outputKey": "1444e10128e2281b23fc8f1b6ef6d804cd1be4c43dbe381b4a0c89bd05111d88",
59
+ "scriptPubKey": "51201444e10128e2281b23fc8f1b6ef6d804cd1be4c43dbe381b4a0c89bd05111d88",
60
+ "scriptHash": "1e943bea5b0c861d787829dda1c2bd981c9c546cfeb5ff7567c94261acfa3548"
61
+ }
62
+ ]
63
+ }
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Strict v2 holder input covenant, Phase 1 prototype.
3
+ *
4
+ * This leaf proves that the current strict-asset input is the canonical CU(X)
5
+ * for witness OWNER_XONLY and that the same transaction spends the unique
6
+ * verifier asset at input 0.
7
+ * The verifier input must separately execute the matching CV policy covenant;
8
+ * this holder leaf is not a complete strict-asset policy by itself.
9
+ *
10
+ * Asset constants use the byte order returned by the Simplicity Elements jets.
11
+ */
12
+ fn not(bit: bool) -> bool {
13
+ <u1>::into(jet::complement_1(<bool>::into(bit)))
14
+ }
15
+
16
+ fn canonical_holder_script_hash(owner: Pubkey) -> u256 {
17
+ let expected_internal_key: Pubkey = 0x{{NUMS_INTERNAL_KEY}};
18
+ assert!(jet::eq_256(jet::internal_key(), expected_internal_key));
19
+
20
+ let user_leaf: u256 = jet::tapleaf_hash();
21
+ let tapdata_ctx: Ctx8 = jet::tapdata_init();
22
+ let tapdata_ctx: Ctx8 = jet::sha_256_ctx_8_add_32(tapdata_ctx, owner);
23
+ let owner_tapdata: u256 = jet::sha_256_ctx_8_finalize(tapdata_ctx);
24
+ let holder_root: u256 = jet::build_tapbranch(user_leaf, owner_tapdata);
25
+ let output_key: u256 = jet::build_taptweak(expected_internal_key, holder_root);
26
+
27
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_init();
28
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(script_ctx, 0x51);
29
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(script_ctx, 0x20);
30
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_add_32(script_ctx, output_key);
31
+ jet::sha_256_ctx_8_finalize(script_ctx)
32
+ }
33
+
34
+ fn explicit_input_asset_amount(index: u32) -> (u256, u64) {
35
+ let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index));
36
+ let (asset, amount): (Asset1, Amount1) = pair;
37
+ let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
38
+ let amount_bits: u64 = unwrap_right::<(u1, u256)>(amount);
39
+ (asset_bits, amount_bits)
40
+ }
41
+
42
+ fn main() {
43
+ let owner: Pubkey = witness::OWNER_XONLY;
44
+ assert!(jet::eq_256(jet::current_script_hash(), canonical_holder_script_hash(owner)));
45
+
46
+ let current_index: u32 = jet::current_index();
47
+ assert!(not(jet::eq_32(current_index, 0)));
48
+
49
+ let (current_asset, current_amount): (u256, u64) =
50
+ explicit_input_asset_amount(current_index);
51
+ let strict_asset: u256 = 0x{{STRICT_ASSET_ID_JET_HEX}};
52
+ assert!(jet::eq_256(current_asset, strict_asset));
53
+ assert!(not(jet::eq_64(current_amount, 0)));
54
+
55
+ let (verifier_asset, verifier_amount): (u256, u64) =
56
+ explicit_input_asset_amount(0);
57
+ let expected_verifier_asset: u256 = 0x{{VERIFIER_ASSET_ID_JET_HEX}};
58
+ assert!(jet::eq_256(verifier_asset, expected_verifier_asset));
59
+ assert!(jet::eq_64(verifier_amount, {{VERIFIER_AMOUNT_ATOMIC}}));
60
+
61
+ jet::bip_0340_verify((owner, jet::sig_all_hash()), witness::HOLDER_SIGNATURE)
62
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "schema": "hazbase_strict_policy_snapshot_golden_vector_v1",
3
+ "name": "issuer_single_v1_regtest_epoch_0",
4
+ "notes": "Public test vector only. The private key value 1 must never be used outside tests.",
5
+ "testPrivateKeyHex": "0000000000000000000000000000000000000000000000000000000000000001",
6
+ "payload": {
7
+ "schema": "hazbase_strict_policy_snapshot_v1",
8
+ "network": "liquid-regtest",
9
+ "liquidGenesisHash": "1111111111111111111111111111111111111111111111111111111111111111",
10
+ "assetBindingId": "strict-card-pilot-1",
11
+ "liquidAssetId": "2222222222222222222222222222222222222222222222222222222222222222",
12
+ "epoch": "0",
13
+ "previousSnapshotHash": null,
14
+ "policyRoot": "3333333333333333333333333333333333333333333333333333333333333333",
15
+ "whitelistRoot": "4444444444444444444444444444444444444444444444444444444444444444",
16
+ "verifier": {
17
+ "outpoint": {
18
+ "txid": "5555555555555555555555555555555555555555555555555555555555555555",
19
+ "vout": 0
20
+ },
21
+ "assetId": "6666666666666666666666666666666666666666666666666666666666666666",
22
+ "amountAtomic": "1",
23
+ "cmr": "7777777777777777777777777777777777777777777777777777777777777777",
24
+ "scriptHash": "8888888888888888888888888888888888888888888888888888888888888888"
25
+ },
26
+ "effectiveAt": "2026-08-19T00:00:00.000Z",
27
+ "validUntil": null
28
+ },
29
+ "authority": {
30
+ "authorityType": "issuer_single",
31
+ "authorityId": "strict-card-pilot-authority",
32
+ "authorityVersion": "1",
33
+ "authoritySetHash": "d2e6e5ed40e4ed2eebd74e30f333f33290721291de77b4b7bbf1856afe41d995",
34
+ "approvalScheme": "bip340-sha256",
35
+ "approvalThreshold": 1,
36
+ "signerXonly": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
37
+ "approvedAt": "2026-08-18T23:59:00.000Z"
38
+ },
39
+ "expected": {
40
+ "canonicalPayload": "{\"assetBindingId\":\"strict-card-pilot-1\",\"effectiveAt\":\"2026-08-19T00:00:00.000Z\",\"epoch\":\"0\",\"liquidAssetId\":\"2222222222222222222222222222222222222222222222222222222222222222\",\"liquidGenesisHash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\"network\":\"liquid-regtest\",\"policyRoot\":\"3333333333333333333333333333333333333333333333333333333333333333\",\"previousSnapshotHash\":null,\"schema\":\"hazbase_strict_policy_snapshot_v1\",\"validUntil\":null,\"verifier\":{\"amountAtomic\":\"1\",\"assetId\":\"6666666666666666666666666666666666666666666666666666666666666666\",\"cmr\":\"7777777777777777777777777777777777777777777777777777777777777777\",\"outpoint\":{\"txid\":\"5555555555555555555555555555555555555555555555555555555555555555\",\"vout\":0},\"scriptHash\":\"8888888888888888888888888888888888888888888888888888888888888888\"},\"whitelistRoot\":\"4444444444444444444444444444444444444444444444444444444444444444\"}",
41
+ "snapshotHash": "1c3994d3bd78e9d8cb48b33be8fc2db4e2da2c9bde11d0bfd26ab493a46942dd",
42
+ "canonicalApprovalMessage": "{\"approvalScheme\":\"bip340-sha256\",\"approvalThreshold\":1,\"approvedAt\":\"2026-08-18T23:59:00.000Z\",\"assetBindingId\":\"strict-card-pilot-1\",\"authorityId\":\"strict-card-pilot-authority\",\"authoritySetHash\":\"d2e6e5ed40e4ed2eebd74e30f333f33290721291de77b4b7bbf1856afe41d995\",\"authorityType\":\"issuer_single\",\"authorityVersion\":\"1\",\"epoch\":\"0\",\"liquidAssetId\":\"2222222222222222222222222222222222222222222222222222222222222222\",\"liquidGenesisHash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\"network\":\"liquid-regtest\",\"previousSnapshotHash\":null,\"schema\":\"hazbase_strict_policy_snapshot_approval_v1\",\"signerXonly\":\"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"snapshotHash\":\"1c3994d3bd78e9d8cb48b33be8fc2db4e2da2c9bde11d0bfd26ab493a46942dd\",\"verifierCmr\":\"7777777777777777777777777777777777777777777777777777777777777777\",\"verifierScriptHash\":\"8888888888888888888888888888888888888888888888888888888888888888\"}",
43
+ "approvalMessageHash": "6535e9cbdf029702754847a1fd7ab5e41a0606793632ab87a0c6a9f24ac59fc4",
44
+ "signatureHex": "14e7c165d70a0cf33cba29cc62bed4c1c55e1adbe537b5f55691e382dbf4d376eb98e48670b7238fddca222cb01b987fa2f217422dc684f8d6c7941ace6912a7",
45
+ "approvalProofHash": "1a657fb2cd581027b2603aad1e19f644de50d4a3ca25e1e10cd036ce3076e91a"
46
+ }
47
+ }
@@ -0,0 +1,240 @@
1
+ /*
2
+ * Strict v2 verifier covenant.
3
+ *
4
+ * One CMR exposes three explicit paths:
5
+ * - ordinary transfer: every strict output is a canonical whitelisted CU(X)
6
+ * - redemption: every strict output is returned to the configured vault
7
+ * - governance: the authority rotates the verifier to a reviewed successor
8
+ *
9
+ * Pilot limits are consensus-visible: at most 16 inputs, at most 8 outputs,
10
+ * and an 8-level owner whitelist proof. Transactions outside these bounds
11
+ * fail closed and require a later reviewed profile.
12
+ */
13
+ fn not(bit: bool) -> bool {
14
+ <u1>::into(jet::complement_1(<bool>::into(bit)))
15
+ }
16
+
17
+ fn explicit_input_asset_amount(index: u32) -> (u256, u64) {
18
+ let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index));
19
+ let (asset, amount): (Asset1, Amount1) = pair;
20
+ let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
21
+ let amount_bits: u64 = unwrap_right::<(u1, u256)>(amount);
22
+ (asset_bits, amount_bits)
23
+ }
24
+
25
+ fn explicit_output_asset_amount(index: u32) -> (u256, u64) {
26
+ let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index));
27
+ let (asset, amount): (Asset1, Amount1) = pair;
28
+ let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
29
+ let amount_bits: u64 = unwrap_right::<(u1, u256)>(amount);
30
+ (asset_bits, amount_bits)
31
+ }
32
+
33
+ fn whitelist_owner_leaf(owner: Pubkey) -> u256 {
34
+ let tag_hash: u256 = 0x{{WHITELIST_OWNER_TAG_HASH}};
35
+ let ctx: Ctx8 = jet::sha_256_ctx_8_init();
36
+ let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, tag_hash);
37
+ let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, tag_hash);
38
+ let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, owner);
39
+ jet::sha_256_ctx_8_finalize(ctx)
40
+ }
41
+
42
+ fn whitelist_step(step: (bool, u256), node: u256) -> u256 {
43
+ let (sibling_on_left, sibling): (bool, u256) = step;
44
+ let tag_hash: u256 = 0x{{WHITELIST_NODE_TAG_HASH}};
45
+ let ctx: Ctx8 = jet::sha_256_ctx_8_init();
46
+ let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, tag_hash);
47
+ let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, tag_hash);
48
+ let ctx: Ctx8 = match sibling_on_left {
49
+ true => jet::sha_256_ctx_8_add_32(ctx, sibling),
50
+ false => jet::sha_256_ctx_8_add_32(ctx, node),
51
+ };
52
+ let ctx: Ctx8 = match sibling_on_left {
53
+ true => jet::sha_256_ctx_8_add_32(ctx, node),
54
+ false => jet::sha_256_ctx_8_add_32(ctx, sibling),
55
+ };
56
+ jet::sha_256_ctx_8_finalize(ctx)
57
+ }
58
+
59
+ fn require_whitelisted(owner: Pubkey, proof: [(bool, u256); {{WHITELIST_DEPTH}}]) {
60
+ let leaf: u256 = whitelist_owner_leaf(owner);
61
+ let root: u256 = array_fold::<whitelist_step, {{WHITELIST_DEPTH}}>(proof, leaf);
62
+ let expected_root: u256 = 0x{{WHITELIST_ROOT}};
63
+ assert!(jet::eq_256(root, expected_root));
64
+ }
65
+
66
+ fn canonical_holder_script_hash(owner: Pubkey) -> u256 {
67
+ let holder_cmr: u256 = 0x{{HOLDER_PROGRAM_CMR}};
68
+ let holder_leaf: u256 = jet::build_tapleaf_simplicity(holder_cmr);
69
+ let tapdata_ctx: Ctx8 = jet::tapdata_init();
70
+ let tapdata_ctx: Ctx8 = jet::sha_256_ctx_8_add_32(tapdata_ctx, owner);
71
+ let owner_tapdata: u256 = jet::sha_256_ctx_8_finalize(tapdata_ctx);
72
+ let holder_root: u256 = jet::build_tapbranch(holder_leaf, owner_tapdata);
73
+ let holder_internal_key: Pubkey = 0x{{HOLDER_NUMS_INTERNAL_KEY}};
74
+ let output_key: u256 = jet::build_taptweak(holder_internal_key, holder_root);
75
+
76
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_init();
77
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(script_ctx, 0x51);
78
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(script_ctx, 0x20);
79
+ let script_ctx: Ctx8 = jet::sha_256_ctx_8_add_32(script_ctx, output_key);
80
+ jet::sha_256_ctx_8_finalize(script_ctx)
81
+ }
82
+
83
+ fn next_index(index: u32) -> u32 {
84
+ let (carry, next): (bool, u32) = jet::add_32(index, 1);
85
+ assert!(not(carry));
86
+ next
87
+ }
88
+
89
+ fn scan_input(unused: (), state: (u32, bool)) -> (u32, bool) {
90
+ let (index, found_strict): (u32, bool) = state;
91
+ let past_end: bool = jet::le_32(jet::num_inputs(), index);
92
+ let found_strict: bool = match past_end {
93
+ true => found_strict,
94
+ false => {
95
+ let (asset, amount): (u256, u64) = explicit_input_asset_amount(index);
96
+ let strict_asset: u256 = 0x{{STRICT_ASSET_ID_JET_HEX}};
97
+ let is_positive: bool = not(jet::eq_64(amount, 0));
98
+ let is_strict: bool = jet::eq_256(asset, strict_asset);
99
+ match is_strict {
100
+ true => is_positive,
101
+ false => found_strict,
102
+ }
103
+ },
104
+ };
105
+ (next_index(index), found_strict)
106
+ }
107
+
108
+ fn scan_output_ordinary(
109
+ owner_proof: (Pubkey, [(bool, u256); {{WHITELIST_DEPTH}}]),
110
+ state: (u32, bool)
111
+ ) -> (u32, bool) {
112
+ let (index, found_strict): (u32, bool) = state;
113
+ let past_end: bool = jet::le_32(jet::num_outputs(), index);
114
+ let found_strict: bool = match past_end {
115
+ true => found_strict,
116
+ false => {
117
+ let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index));
118
+ let (asset, amount): (Asset1, Amount1) = pair;
119
+ let asset: u256 = unwrap_right::<(u1, u256)>(asset);
120
+ let unused_amount: u64 = unwrap_right::<(u1, u256)>(amount);
121
+ let strict_asset: u256 = 0x{{STRICT_ASSET_ID_JET_HEX}};
122
+ match jet::eq_256(asset, strict_asset) {
123
+ true => {
124
+ let (owner, proof): (Pubkey, [(bool, u256); {{WHITELIST_DEPTH}}]) = owner_proof;
125
+ require_whitelisted(owner, proof);
126
+ let actual_script_hash: u256 = unwrap(jet::output_script_hash(index));
127
+ let expected_script_hash: u256 = canonical_holder_script_hash(owner);
128
+ assert!(jet::eq_256(actual_script_hash, expected_script_hash));
129
+ true
130
+ },
131
+ false => found_strict,
132
+ }
133
+ },
134
+ };
135
+ (next_index(index), found_strict)
136
+ }
137
+
138
+ fn require_verifier_successor(expected_script_hash: u256) {
139
+ let (verifier_asset, verifier_amount): (u256, u64) = explicit_input_asset_amount(0);
140
+ let expected_verifier_asset: u256 = 0x{{VERIFIER_ASSET_ID_JET_HEX}};
141
+ assert!(jet::eq_256(verifier_asset, expected_verifier_asset));
142
+ assert!(jet::eq_64(verifier_amount, {{VERIFIER_AMOUNT_ATOMIC}}));
143
+
144
+ let (successor_asset, successor_amount): (u256, u64) = explicit_output_asset_amount(0);
145
+ assert!(jet::eq_256(successor_asset, expected_verifier_asset));
146
+ assert!(jet::eq_64(successor_amount, {{VERIFIER_AMOUNT_ATOMIC}}));
147
+ let successor_script_hash: u256 = unwrap(jet::output_script_hash(0));
148
+ assert!(jet::eq_256(successor_script_hash, expected_script_hash))
149
+ }
150
+
151
+ fn scan_output_redemption(unused: (), state: (u32, bool)) -> (u32, bool) {
152
+ let (index, found_strict): (u32, bool) = state;
153
+ let past_end: bool = jet::le_32(jet::num_outputs(), index);
154
+ let found_strict: bool = match past_end {
155
+ true => found_strict,
156
+ false => {
157
+ let (asset, unused_amount): (u256, u64) = explicit_output_asset_amount(index);
158
+ let strict_asset: u256 = 0x{{STRICT_ASSET_ID_JET_HEX}};
159
+ match jet::eq_256(asset, strict_asset) {
160
+ true => {
161
+ let actual_script_hash: u256 = unwrap(jet::output_script_hash(index));
162
+ let redemption_script_hash: u256 = 0x{{REDEMPTION_SCRIPT_HASH}};
163
+ assert!(jet::eq_256(actual_script_hash, redemption_script_hash));
164
+ true
165
+ },
166
+ false => found_strict,
167
+ }
168
+ },
169
+ };
170
+ (next_index(index), found_strict)
171
+ }
172
+
173
+ fn strict_input_present() -> bool {
174
+ let input_slots: [(); {{MAX_INPUTS}}] = [{{INPUT_UNIT_SLOTS}}];
175
+ let input_state: (u32, bool) =
176
+ array_fold::<scan_input, {{MAX_INPUTS}}>(input_slots, (0, false));
177
+ let (unused_input_index, found_strict_input): (u32, bool) = input_state;
178
+ found_strict_input
179
+ }
180
+
181
+ fn ordinary_transfer(
182
+ output_owner_proofs: [(Pubkey, [(bool, u256); {{WHITELIST_DEPTH}}]); {{MAX_OUTPUTS}}]
183
+ ) {
184
+ require_verifier_successor(jet::current_script_hash());
185
+ assert!(strict_input_present());
186
+
187
+ let output_state: (u32, bool) =
188
+ array_fold::<scan_output_ordinary, {{MAX_OUTPUTS}}>(output_owner_proofs, (0, false));
189
+ let (unused_output_index, found_strict_output): (u32, bool) = output_state;
190
+ assert!(found_strict_output)
191
+ }
192
+
193
+ fn redemption(authority_signature: Signature) {
194
+ require_verifier_successor(jet::current_script_hash());
195
+ assert!(strict_input_present());
196
+
197
+ let output_slots: [(); {{MAX_OUTPUTS}}] = [{{OUTPUT_UNIT_SLOTS}}];
198
+ let output_state: (u32, bool) =
199
+ array_fold::<scan_output_redemption, {{MAX_OUTPUTS}}>(output_slots, (0, false));
200
+ let (unused_output_index, found_strict_output): (u32, bool) = output_state;
201
+ assert!(found_strict_output);
202
+
203
+ let authority: Pubkey = 0x{{AUTHORITY_XONLY}};
204
+ jet::bip_0340_verify((authority, jet::sig_all_hash()), authority_signature)
205
+ }
206
+
207
+ fn governance(next: (u256, Signature)) {
208
+ let (next_verifier_script_hash, authority_signature): (u256, Signature) = next;
209
+ require_verifier_successor(next_verifier_script_hash);
210
+ assert!(not(strict_input_present()));
211
+
212
+ let output_slots: [(); {{MAX_OUTPUTS}}] = [{{OUTPUT_UNIT_SLOTS}}];
213
+ let output_state: (u32, bool) =
214
+ array_fold::<scan_output_redemption, {{MAX_OUTPUTS}}>(output_slots, (0, false));
215
+ let (unused_output_index, found_strict_output): (u32, bool) = output_state;
216
+ assert!(not(found_strict_output));
217
+
218
+ let authority: Pubkey = 0x{{AUTHORITY_XONLY}};
219
+ jet::bip_0340_verify((authority, jet::sig_all_hash()), authority_signature)
220
+ }
221
+
222
+ fn main() {
223
+ assert!(jet::eq_32(jet::current_index(), 0));
224
+ assert!(jet::le_32(jet::num_inputs(), {{MAX_INPUTS}}));
225
+ assert!(jet::le_32(jet::num_outputs(), {{MAX_OUTPUTS}}));
226
+
227
+ let action:
228
+ Either<
229
+ [(Pubkey, [(bool, u256); {{WHITELIST_DEPTH}}]); {{MAX_OUTPUTS}}],
230
+ Either<Signature, (u256, Signature)>
231
+ > = witness::ACTION;
232
+ match action {
233
+ Left(output_owner_proofs : [(Pubkey, [(bool, u256); {{WHITELIST_DEPTH}}]); {{MAX_OUTPUTS}}]) =>
234
+ ordinary_transfer(output_owner_proofs),
235
+ Right(protected_action : Either<Signature, (u256, Signature)>) => match protected_action {
236
+ Left(authority_signature : Signature) => redemption(authority_signature),
237
+ Right(next : (u256, Signature)) => governance(next),
238
+ },
239
+ }
240
+ }