@crediolabs/policy-synth 0.2.0 → 0.3.1
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 +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/record/freshness.d.ts +17 -11
- package/dist/record/freshness.js +28 -18
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/record/freshness.d.ts +17 -11
- package/dist-cjs/record/freshness.js +28 -18
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/package.json +5 -5
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/record/freshness.ts +29 -18
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/adapters/interpreter/index.d.ts +0 -1
- package/dist/adapters/interpreter/index.js +0 -2
- package/dist/adapters/oz/adapter.d.ts +0 -20
- package/dist/adapters/oz/adapter.js +0 -295
- package/dist/adapters/oz/index.d.ts +0 -1
- package/dist/adapters/oz/index.js +0 -2
- package/dist/codegen/compile-gate.d.ts +0 -33
- package/dist/codegen/compile-gate.js +0 -124
- package/dist/codegen/index.d.ts +0 -2
- package/dist/codegen/index.js +0 -8
- package/dist/codegen/template.d.ts +0 -18
- package/dist/codegen/template.js +0 -148
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist/ir/index.d.ts +0 -1
- package/dist/ir/index.js +0 -2
- package/dist/ir/types.d.ts +0 -140
- package/dist/ir/types.js +0 -11
- package/dist/mandate/index.d.ts +0 -2
- package/dist/mandate/index.js +0 -2
- package/dist/mandate/to-ir.d.ts +0 -3
- package/dist/mandate/to-ir.js +0 -60
- package/dist/mandate/types.d.ts +0 -20
- package/dist/mandate/types.js +0 -8
- package/dist/seams/index.d.ts +0 -1
- package/dist/seams/index.js +0 -2
- package/dist/seams/types.d.ts +0 -66
- package/dist/seams/types.js +0 -11
- package/dist/synth/deny-cases.js +0 -562
- package/dist/synth/evaluate.d.ts +0 -39
- package/dist/synth/evaluate.js +0 -551
- package/dist/synth/harness.d.ts +0 -28
- package/dist/synth/harness.js +0 -47
- package/dist/synth/minimize.d.ts +0 -4
- package/dist/synth/minimize.js +0 -38
- package/dist/synth/permit-context.d.ts +0 -15
- package/dist/synth/permit-context.js +0 -116
- package/dist/synth/predicate-literals.d.ts +0 -5
- package/dist/synth/predicate-literals.js +0 -25
- package/dist/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist/synth/synthesize-from-mandate.js +0 -59
- package/dist/verify/envelope.d.ts +0 -15
- package/dist/verify/envelope.js +0 -22
- package/dist/verify/index.d.ts +0 -3
- package/dist/verify/index.js +0 -3
- package/dist/verify/simulate.d.ts +0 -31
- package/dist/verify/simulate.js +0 -258
- package/dist/verify/verify.d.ts +0 -21
- package/dist/verify/verify.js +0 -189
- package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
- package/dist-cjs/adapters/interpreter/index.js +0 -8
- package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
- package/dist-cjs/adapters/oz/adapter.js +0 -300
- package/dist-cjs/adapters/oz/index.d.ts +0 -1
- package/dist-cjs/adapters/oz/index.js +0 -8
- package/dist-cjs/codegen/compile-gate.d.ts +0 -33
- package/dist-cjs/codegen/compile-gate.js +0 -128
- package/dist-cjs/codegen/index.d.ts +0 -2
- package/dist-cjs/codegen/index.js +0 -14
- package/dist-cjs/codegen/template.d.ts +0 -18
- package/dist-cjs/codegen/template.js +0 -151
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/dist-cjs/ir/index.d.ts +0 -1
- package/dist-cjs/ir/index.js +0 -3
- package/dist-cjs/ir/types.d.ts +0 -140
- package/dist-cjs/ir/types.js +0 -12
- package/dist-cjs/mandate/index.d.ts +0 -2
- package/dist-cjs/mandate/index.js +0 -6
- package/dist-cjs/mandate/to-ir.d.ts +0 -3
- package/dist-cjs/mandate/to-ir.js +0 -63
- package/dist-cjs/mandate/types.d.ts +0 -20
- package/dist-cjs/mandate/types.js +0 -9
- package/dist-cjs/seams/index.d.ts +0 -1
- package/dist-cjs/seams/index.js +0 -3
- package/dist-cjs/seams/types.d.ts +0 -66
- package/dist-cjs/seams/types.js +0 -12
- package/dist-cjs/synth/deny-cases.js +0 -568
- package/dist-cjs/synth/evaluate.d.ts +0 -39
- package/dist-cjs/synth/evaluate.js +0 -554
- package/dist-cjs/synth/harness.d.ts +0 -28
- package/dist-cjs/synth/harness.js +0 -50
- package/dist-cjs/synth/minimize.d.ts +0 -4
- package/dist-cjs/synth/minimize.js +0 -41
- package/dist-cjs/synth/permit-context.d.ts +0 -15
- package/dist-cjs/synth/permit-context.js +0 -119
- package/dist-cjs/synth/predicate-literals.d.ts +0 -5
- package/dist-cjs/synth/predicate-literals.js +0 -28
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
- package/dist-cjs/verify/envelope.d.ts +0 -15
- package/dist-cjs/verify/envelope.js +0 -23
- package/dist-cjs/verify/index.d.ts +0 -3
- package/dist-cjs/verify/index.js +0 -8
- package/dist-cjs/verify/simulate.d.ts +0 -31
- package/dist-cjs/verify/simulate.js +0 -261
- package/dist-cjs/verify/verify.d.ts +0 -21
- package/dist-cjs/verify/verify.js +0 -192
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
package/src/synth/evaluate.ts
DELETED
|
@@ -1,613 +0,0 @@
|
|
|
1
|
-
// src/synth/evaluate.ts - the model evaluator (TS model of interpreter semantics).
|
|
2
|
-
//
|
|
3
|
-
// Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
|
|
4
|
-
// no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
|
|
5
|
-
// 1. ledger expiry -> 'EXPIRED'
|
|
6
|
-
// 2. `now` past valid_until -> 'EXPIRED'
|
|
7
|
-
// 3. contract mismatch -> 'CONTRACT_SCOPE'
|
|
8
|
-
// 4. per-ScVal equality on fn/args; EXACT ordered vector
|
|
9
|
-
// equality; fail-closed on opaque args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
10
|
-
// 5. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
11
|
-
// 6. amount / window_spent via BigInt -> 'AMOUNT_BOUND'
|
|
12
|
-
// 7. invocation_count_in_window -> 'FREQUENCY'
|
|
13
|
-
// 8. oracle_price: fatal -> throw OracleError, deny with
|
|
14
|
-
// underlying ORACLE_* reason -> 'ORACLE_*'
|
|
15
|
-
// 9. boolean nodes (and/or/not)
|
|
16
|
-
// 10. signer threshold gate -> 'THRESHOLD_NOT_MET'
|
|
17
|
-
// 11. otherwise permit.
|
|
18
|
-
//
|
|
19
|
-
// Amounts: BigInt on decimal strings. Oracle errors thrown (never
|
|
20
|
-
// boolean-false `not`/`or` could mask).
|
|
21
|
-
|
|
22
|
-
import type { PredicateLeaf, PredicateNode, ScVal } from '../types.ts'
|
|
23
|
-
import { literalNumericBigInt } from './predicate-literals.ts'
|
|
24
|
-
|
|
25
|
-
/** Oracle prices normalise to this many decimals; mirrors NORMALISED_DECIMALS
|
|
26
|
-
* in oracle.rs. A threshold on any other basis must say so. */
|
|
27
|
-
const NORMALISED_DECIMALS = 9
|
|
28
|
-
/** Mirrors MAX_ORACLE_THRESHOLD_DECIMALS in dsl.rs. */
|
|
29
|
-
const MAX_ORACLE_THRESHOLD_DECIMALS = 18
|
|
30
|
-
|
|
31
|
-
export interface EvalContext {
|
|
32
|
-
/** Contract the interpreter is asked to enforce against. */
|
|
33
|
-
contract: string
|
|
34
|
-
/** Function name on that contract. */
|
|
35
|
-
fn: string
|
|
36
|
-
/** Decoded `ScVal[]` of the top-level authorized call. */
|
|
37
|
-
args: ScVal[]
|
|
38
|
-
/** Ledger sequence at which the interpreter is invoked. */
|
|
39
|
-
atLedger: number
|
|
40
|
-
/** Optional policy expiry; absent -> step 1 is skipped. */
|
|
41
|
-
validUntilLedger?: number
|
|
42
|
-
/** Unix seconds; used by `now` / `valid_until` leaves. */
|
|
43
|
-
nowSeconds: number
|
|
44
|
-
/** Per-token amount moved by the current call (i128 decimal string). */
|
|
45
|
-
amountByToken: Record<string, string>
|
|
46
|
-
/** Per-token window-rolling spend prior to this call (i128 decimal string). */
|
|
47
|
-
windowSpentByToken: Record<string, string>
|
|
48
|
-
/** Recorded invocation counts keyed by window seconds. */
|
|
49
|
-
invocationCountByWindow: Record<number, number>
|
|
50
|
-
/** Optional signer-weight map for the threshold gate. Absent -> skip. */
|
|
51
|
-
signerWeights?: Record<string, number>
|
|
52
|
-
/** Per-asset oracle snapshot. Missing keys default to ORACLE_STALE. */
|
|
53
|
-
oraclePriceByAsset: Record<
|
|
54
|
-
string,
|
|
55
|
-
| { price: string; timestampSeconds: number }
|
|
56
|
-
| { error: 'stale' | 'missing' | 'deviation' | 'paused' | 'decimals' | 'fingerprint' }
|
|
57
|
-
>
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type EvalResult = { permit: true } | { permit: false; reason: string }
|
|
61
|
-
|
|
62
|
-
/** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
|
|
63
|
-
* and converted to the matching `ORACLE_*` deny reason. NOT a boolean-false
|
|
64
|
-
* that `not` / `or` could mask. */
|
|
65
|
-
class OracleError extends Error {
|
|
66
|
-
readonly code: string
|
|
67
|
-
constructor(code: string) {
|
|
68
|
-
super(`oracle fatal: ${code}`)
|
|
69
|
-
this.code = code
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const ORACLE_ERROR_CODES: Readonly<Record<string, string>> = {
|
|
74
|
-
stale: 'ORACLE_STALE',
|
|
75
|
-
missing: 'ORACLE_MISSING',
|
|
76
|
-
deviation: 'ORACLE_DEVIATION_EXCEEDED',
|
|
77
|
-
paused: 'ORACLE_PAUSED',
|
|
78
|
-
decimals: 'ORACLE_DECIMALS_MISMATCH',
|
|
79
|
-
fingerprint: 'ORACLE_FINGERPRINT_DRIFT',
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
|
|
83
|
-
* Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
|
|
84
|
-
export function evaluate(predicate: PredicateNode, ctx: EvalContext): EvalResult {
|
|
85
|
-
try {
|
|
86
|
-
// --- step 1: ledger-time expiry ---
|
|
87
|
-
if (ctx.validUntilLedger !== undefined && ctx.atLedger > ctx.validUntilLedger) {
|
|
88
|
-
return { permit: false, reason: 'EXPIRED' }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// --- step 2..9: predicate tree ---
|
|
92
|
-
const decision = walk(predicate, ctx)
|
|
93
|
-
|
|
94
|
-
// --- step 10: signer threshold gate (only when the predicate permitted) ---
|
|
95
|
-
if (decision.permit && ctx.signerWeights !== undefined) {
|
|
96
|
-
let totalWeight = 0n
|
|
97
|
-
for (const w of Object.values(ctx.signerWeights)) {
|
|
98
|
-
// weights are non-negative integers; BigInt keeps the gate bounded
|
|
99
|
-
totalWeight += BigInt(w)
|
|
100
|
-
}
|
|
101
|
-
if (totalWeight === 0n) return { permit: false, reason: 'THRESHOLD_NOT_MET' }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return decision
|
|
105
|
-
} catch (e) {
|
|
106
|
-
if (e instanceof OracleError) {
|
|
107
|
-
return { permit: false, reason: e.code }
|
|
108
|
-
}
|
|
109
|
-
throw e
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Walk the predicate tree. Returns the FIRST deny reason encountered on
|
|
114
|
-
* the active branch (so `and` fails-fast; `or` accepts the first permit). */
|
|
115
|
-
function walk(node: PredicateNode, ctx: EvalContext): EvalResult {
|
|
116
|
-
switch (node.op) {
|
|
117
|
-
case 'and': {
|
|
118
|
-
let lastDeny: EvalResult | null = null
|
|
119
|
-
for (const child of node.children) {
|
|
120
|
-
const r = walk(child, ctx)
|
|
121
|
-
if (!r.permit) {
|
|
122
|
-
// deny-on-first: short-circuit on the failing child.
|
|
123
|
-
return r
|
|
124
|
-
}
|
|
125
|
-
lastDeny = r
|
|
126
|
-
}
|
|
127
|
-
return lastDeny ?? { permit: true }
|
|
128
|
-
}
|
|
129
|
-
case 'or': {
|
|
130
|
-
let lastDeny: EvalResult | null = null
|
|
131
|
-
for (const child of node.children) {
|
|
132
|
-
const r = walk(child, ctx)
|
|
133
|
-
if (r.permit) return r
|
|
134
|
-
lastDeny = r
|
|
135
|
-
}
|
|
136
|
-
return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
137
|
-
}
|
|
138
|
-
case 'not': {
|
|
139
|
-
// `not` structurally inverts the child unless the child contains an
|
|
140
|
-
// oracle leaf (compile-time rule: no oracle leaf under not/or). A
|
|
141
|
-
// child `OracleError` is re-thrown to the catch at the top of `evaluate`.
|
|
142
|
-
const r = walk(node.child, ctx)
|
|
143
|
-
if (r.permit) return { permit: false, reason: 'FN_MISMATCH' }
|
|
144
|
-
return { permit: true }
|
|
145
|
-
}
|
|
146
|
-
case 'eq':
|
|
147
|
-
case 'lt':
|
|
148
|
-
case 'lte':
|
|
149
|
-
case 'gt':
|
|
150
|
-
case 'gte':
|
|
151
|
-
return evalCompare(node.op, node.left, node.right, ctx)
|
|
152
|
-
case 'in':
|
|
153
|
-
return evalIn(node.needle, node.haystack, ctx)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Step 2..8: comparison leaf evaluation. */
|
|
158
|
-
function evalCompare(
|
|
159
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
160
|
-
left: PredicateLeaf,
|
|
161
|
-
right: PredicateLeaf,
|
|
162
|
-
ctx: EvalContext
|
|
163
|
-
): EvalResult {
|
|
164
|
-
// --- step 2: `now` vs `valid_until` semantics ---
|
|
165
|
-
if (left.kind === 'now' && right.kind === 'valid_until') {
|
|
166
|
-
const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0
|
|
167
|
-
// `valid_until` is modelled as a synthetic future timestamp far past
|
|
168
|
-
// `nowSeconds` so the only true-positive expired path is the `gt`/`gte`
|
|
169
|
-
// shapes callers actually write.
|
|
170
|
-
if (op === 'gt' || op === 'gte') {
|
|
171
|
-
if (ctx.nowSeconds > 0) return { permit: false, reason: 'EXPIRED' }
|
|
172
|
-
} else if (op === 'lt' || op === 'lte') {
|
|
173
|
-
return { permit: true }
|
|
174
|
-
}
|
|
175
|
-
return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true }
|
|
176
|
-
}
|
|
177
|
-
if (right.kind === 'now' && left.kind === 'valid_until') {
|
|
178
|
-
return evalCompare(op, right, left, ctx)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// --- step 3: CONTRACT_SCOPE on call_contract eq ---
|
|
182
|
-
if (left.kind === 'call_contract' && op === 'eq') {
|
|
183
|
-
if (right.kind !== 'literal_address') return { permit: false, reason: 'CONTRACT_SCOPE' }
|
|
184
|
-
return right.value === ctx.contract
|
|
185
|
-
? { permit: true }
|
|
186
|
-
: { permit: false, reason: 'CONTRACT_SCOPE' }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// --- step 4a: call_fn equality ---
|
|
190
|
-
if (left.kind === 'call_fn' && op === 'eq') {
|
|
191
|
-
if (right.kind !== 'literal_symbol') return { permit: false, reason: 'FN_MISMATCH' }
|
|
192
|
-
return right.value === ctx.fn ? { permit: true } : { permit: false, reason: 'FN_MISMATCH' }
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
|
|
196
|
-
if (left.kind === 'call_arg') {
|
|
197
|
-
// The swap's canonical form is `call_arg[out] >= call_arg_scaled(in, num, den)`.
|
|
198
|
-
// Dispatched here so the floor's dedicated reason codes (ARITHMETIC_OVERFLOW
|
|
199
|
-
// -> SLIPPAGE_FLOOR) reach the user; routing the scaled RHS through the
|
|
200
|
-
// generic `evalArgOrderedCompare` would mask overflow as ARG_MISMATCH.
|
|
201
|
-
if (right.kind === 'call_arg_scaled') {
|
|
202
|
-
return evalScaledArgCompare(op, left, right, ctx)
|
|
203
|
-
}
|
|
204
|
-
const actual = ctx.args[left.index]
|
|
205
|
-
if (op !== 'eq') return evalArgOrderedCompare(op, actual, right)
|
|
206
|
-
return evalArgEq(op, actual, right, ctx)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// --- step 4b': scaled leaf on the LEFT (`call_arg_scaled(in, num, den) <= call_arg[out]`).
|
|
210
|
-
// Symmetric form so a policy that phrases the floor either way works.
|
|
211
|
-
if (left.kind === 'call_arg_scaled') {
|
|
212
|
-
return evalScaledArgCompare(op, right, left, ctx)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --- step 4c: call_arg_len: length of a vec-typed argument as u32.
|
|
216
|
-
// Fails closed on a non-vec / absent arg or a non-u32 literal.
|
|
217
|
-
if (left.kind === 'call_arg_len') {
|
|
218
|
-
const actual = ctx.args[left.index]
|
|
219
|
-
if (actual?.type !== 'vec') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
220
|
-
if (right.kind !== 'literal_u32') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
221
|
-
return actual.value.length === right.value
|
|
222
|
-
? { permit: true }
|
|
223
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// --- step 4d: call_arg_field: value of a field in the map at element i of
|
|
227
|
-
// the vec at argument index. Fails closed on shape / type / range issues.
|
|
228
|
-
if (left.kind === 'call_arg_field') {
|
|
229
|
-
const actual = ctx.args[left.index]
|
|
230
|
-
if (actual?.type !== 'vec') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
231
|
-
const element = actual.value[left.element]
|
|
232
|
-
if (element?.type !== 'map') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
233
|
-
if (!Array.isArray(element.value)) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
234
|
-
const entry = element.value.find((e) => e.key === left.field)
|
|
235
|
-
if (!entry) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
236
|
-
if (op === 'eq') return evalArgEq(op, entry.val, right, ctx)
|
|
237
|
-
return evalArgOrderedCompare(op, entry.val, right)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// --- step 6: AMOUNT_BOUND ---
|
|
241
|
-
if (left.kind === 'amount' && op !== 'eq') {
|
|
242
|
-
return evalAmountCompare(op, left.token, right, ctx, 'amountByToken')
|
|
243
|
-
}
|
|
244
|
-
if (left.kind === 'window_spent' && op !== 'eq') {
|
|
245
|
-
return evalWindowSpentCompare(op, left.token, right, ctx)
|
|
246
|
-
}
|
|
247
|
-
// `eq` on amount / window_spent not defined as a bound - fall through.
|
|
248
|
-
|
|
249
|
-
// --- step 7: FREQUENCY ---
|
|
250
|
-
if (left.kind === 'invocation_count_in_window') {
|
|
251
|
-
return evalFrequencyCompare(op, left.windowSecs, right, ctx)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// --- step 8: oracle_price (FATAL via throw) ---
|
|
255
|
-
if (left.kind === 'oracle_price') {
|
|
256
|
-
return evalOracleCompare(op, left.asset, right, ctx)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Unknown leaf/op combination - structural fail-closed.
|
|
260
|
-
return { permit: false, reason: 'FN_MISMATCH' }
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Step 4b: per-ScVal equality. Handles literal_vec as an EXACT ordered
|
|
264
|
-
* sequence: compare element-by-element in order; deny if length or any
|
|
265
|
-
* element differs. Opaque args (`type: 'other'`) fail closed. */
|
|
266
|
-
function evalArgEq(
|
|
267
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
268
|
-
actual: ScVal | undefined,
|
|
269
|
-
right: PredicateLeaf,
|
|
270
|
-
ctx: EvalContext
|
|
271
|
-
): EvalResult {
|
|
272
|
-
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality.
|
|
273
|
-
if (op === 'eq' && right.kind === 'literal_vec') {
|
|
274
|
-
if (actual?.type !== 'vec') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
275
|
-
return compareVecExact(actual.value, right.elements, ctx)
|
|
276
|
-
? { permit: true }
|
|
277
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (op === 'eq' && right.kind === 'literal_address') {
|
|
281
|
-
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
282
|
-
if (actual.type === 'other') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
283
|
-
return actual.type === 'address' && actual.value === right.value
|
|
284
|
-
? { permit: true }
|
|
285
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (op === 'eq' && right.kind === 'literal_i128') {
|
|
289
|
-
if (actual?.type !== 'i128') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
290
|
-
return BigInt(actual.value) === BigInt(right.value)
|
|
291
|
-
? { permit: true }
|
|
292
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (op === 'eq' && right.kind === 'literal_symbol') {
|
|
296
|
-
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
297
|
-
return actual.type === 'symbol' && actual.value === right.value
|
|
298
|
-
? { permit: true }
|
|
299
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (op === 'eq' && right.kind === 'literal_u32') {
|
|
303
|
-
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
304
|
-
return actual.type === 'u32' && actual.value === String(right.value)
|
|
305
|
-
? { permit: true }
|
|
306
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (op === 'eq' && right.kind === 'literal_u64') {
|
|
310
|
-
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
311
|
-
return actual.type === 'u64' && actual.value === right.value
|
|
312
|
-
? { permit: true }
|
|
313
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (op === 'eq' && right.kind === 'literal_bytes') {
|
|
317
|
-
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
318
|
-
return actual.type === 'bytes' && actual.value === right.value
|
|
319
|
-
? { permit: true }
|
|
320
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Anything else: fail closed on opacity (cannot decode the arg reliably).
|
|
324
|
-
if (!actual || actual.type === 'other') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
325
|
-
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare`:
|
|
329
|
-
* the scaled leaf is `args[index] * num / den` (truncating toward zero). On
|
|
330
|
-
* overflow or divide-by-zero deny with `ARITHMETIC_OVERFLOW`; a failed bound
|
|
331
|
-
* denies with `SLIPPAGE_FLOOR` (the dedicated reason) rather than the generic
|
|
332
|
-
* `ARG_MISMATCH`. A scaled-on-scaled compare denies `ARG_MISMATCH`
|
|
333
|
-
* (no definable semantics). The non-scaled operand must be numeric
|
|
334
|
-
* (`call_arg` carrying a number, or a numeric literal); else `ARG_MISMATCH`. */
|
|
335
|
-
function evalScaledArgCompare(
|
|
336
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
337
|
-
left: PredicateLeaf,
|
|
338
|
-
right: PredicateLeaf,
|
|
339
|
-
ctx: EvalContext
|
|
340
|
-
): EvalResult {
|
|
341
|
-
let scaled: { index: number; num: string; den: string }
|
|
342
|
-
let other: PredicateLeaf
|
|
343
|
-
let scaledOnRight: boolean
|
|
344
|
-
if (left.kind === 'call_arg_scaled') {
|
|
345
|
-
scaled = left
|
|
346
|
-
other = right
|
|
347
|
-
scaledOnRight = false
|
|
348
|
-
} else if (right.kind === 'call_arg_scaled') {
|
|
349
|
-
scaled = right
|
|
350
|
-
other = left
|
|
351
|
-
scaledOnRight = true
|
|
352
|
-
} else {
|
|
353
|
-
// Programming error in the dispatcher; fail closed.
|
|
354
|
-
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
355
|
-
}
|
|
356
|
-
// Out-of-bounds or non-numeric arg fails closed as ARG_MISMATCH (not
|
|
357
|
-
// SLIPPAGE_FLOOR) - a violated floor is the wrong code when the operand
|
|
358
|
-
// itself could not be read.
|
|
359
|
-
if (scaled.index >= ctx.args.length) {
|
|
360
|
-
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
361
|
-
}
|
|
362
|
-
const input = argNumericBigInt(ctx.args[scaled.index])
|
|
363
|
-
if (input === null) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
364
|
-
let num: bigint
|
|
365
|
-
let den: bigint
|
|
366
|
-
try {
|
|
367
|
-
num = BigInt(scaled.num)
|
|
368
|
-
den = BigInt(scaled.den)
|
|
369
|
-
} catch {
|
|
370
|
-
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
371
|
-
}
|
|
372
|
-
// Install refuses `den == 0` and `num <= 0` / `den <= 0`. The runtime
|
|
373
|
-
// check is defensive belt-and-braces so a validator regression cannot
|
|
374
|
-
// panic the frame on a divide-by-zero.
|
|
375
|
-
if (den === 0n) return { permit: false, reason: 'ARITHMETIC_OVERFLOW' }
|
|
376
|
-
const product = input * num
|
|
377
|
-
let scaledValue: bigint
|
|
378
|
-
try {
|
|
379
|
-
// BigInt division truncates toward zero (matches Rust `i128::checked_div`).
|
|
380
|
-
scaledValue = product / den
|
|
381
|
-
} catch {
|
|
382
|
-
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' }
|
|
383
|
-
}
|
|
384
|
-
// i128 range check: the contract would have wrapped on i128 arithmetic;
|
|
385
|
-
// surface the same deny.
|
|
386
|
-
const I128_MAX = (1n << 127n) - 1n
|
|
387
|
-
const I128_MIN = -(1n << 127n)
|
|
388
|
-
if (scaledValue > I128_MAX || scaledValue < I128_MIN) {
|
|
389
|
-
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' }
|
|
390
|
-
}
|
|
391
|
-
let otherVal: bigint | null
|
|
392
|
-
if (other.kind === 'call_arg') {
|
|
393
|
-
otherVal = argNumericBigInt(ctx.args[other.index])
|
|
394
|
-
} else {
|
|
395
|
-
otherVal = literalNumericBigInt(other)
|
|
396
|
-
}
|
|
397
|
-
if (otherVal === null) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
398
|
-
const pass = scaledOnRight
|
|
399
|
-
? bigintCmp(op, otherVal.toString(), scaledValue.toString())
|
|
400
|
-
: bigintCmp(op, scaledValue.toString(), otherVal.toString())
|
|
401
|
-
return pass ? { permit: true } : { permit: false, reason: 'SLIPPAGE_FLOOR' }
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
|
|
405
|
-
* reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
|
|
406
|
-
* surface) and compares it to a numeric literal via BigInt. A non-numeric arg
|
|
407
|
-
* or a non-numeric literal fails closed (ARG_MISMATCH) rather than permitting
|
|
408
|
-
* an undecidable bound. Backs the SoroSwap input-amount cap
|
|
409
|
-
* (`call_arg[0] <= limit`). */
|
|
410
|
-
function evalArgOrderedCompare(
|
|
411
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
412
|
-
actual: ScVal | undefined,
|
|
413
|
-
right: PredicateLeaf
|
|
414
|
-
): EvalResult {
|
|
415
|
-
const actualInt = argNumericBigInt(actual)
|
|
416
|
-
const literalInt = literalNumericBigInt(right)
|
|
417
|
-
if (actualInt === null || literalInt === null) {
|
|
418
|
-
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
419
|
-
}
|
|
420
|
-
return bigintCmp(op, actualInt.toString(), literalInt.toString())
|
|
421
|
-
? { permit: true }
|
|
422
|
-
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/** BigInt value of a numeric-integer ScVal arg (i128 / u64 / u32), or null when
|
|
426
|
-
* the arg is absent, opaque, or a non-numeric type. */
|
|
427
|
-
function argNumericBigInt(actual: ScVal | undefined): bigint | null {
|
|
428
|
-
if (!actual) return null
|
|
429
|
-
if (actual.type === 'i128' || actual.type === 'u64' || actual.type === 'u32') {
|
|
430
|
-
try {
|
|
431
|
-
return BigInt(actual.value)
|
|
432
|
-
} catch {
|
|
433
|
-
return null
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return null
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** Element-by-element ordered comparison of an `ScVal[]` against a
|
|
440
|
-
* `PredicateLeaf[]`. Equal-length and equal at every index => permit.
|
|
441
|
-
* Length mismatch OR any element mismatch => deny ARG_MISMATCH. */
|
|
442
|
-
function compareVecExact(actual: ScVal[], expected: PredicateLeaf[], ctx: EvalContext): boolean {
|
|
443
|
-
if (actual.length !== expected.length) return false
|
|
444
|
-
for (let i = 0; i < expected.length; i++) {
|
|
445
|
-
const e = expected[i]
|
|
446
|
-
const a = actual[i]
|
|
447
|
-
if (!e || !a) return false
|
|
448
|
-
const r = evalArgEq('eq', a, e, ctx)
|
|
449
|
-
if (!r.permit) return false
|
|
450
|
-
}
|
|
451
|
-
return true
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/** Step 5: `in` membership. Empty haystack ALWAYS denies. Opaque needle
|
|
455
|
-
* fails closed. */
|
|
456
|
-
function evalIn(needle: PredicateLeaf, haystack: PredicateLeaf[], ctx: EvalContext): EvalResult {
|
|
457
|
-
if (haystack.length === 0) return { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
458
|
-
const actual = resolveLeaf(needle, ctx)
|
|
459
|
-
if (!actual || actual.type === 'other') return { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
460
|
-
for (const h of haystack) {
|
|
461
|
-
const r = evalArgEq('eq', actual, h, ctx)
|
|
462
|
-
if (r.permit) return { permit: true }
|
|
463
|
-
}
|
|
464
|
-
return { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/** Resolve a selector leaf to its current ScVal against the candidate call. */
|
|
468
|
-
function resolveLeaf(leaf: PredicateLeaf, ctx: EvalContext): ScVal | undefined {
|
|
469
|
-
switch (leaf.kind) {
|
|
470
|
-
case 'call_contract':
|
|
471
|
-
return { type: 'address', value: ctx.contract }
|
|
472
|
-
case 'call_fn':
|
|
473
|
-
return { type: 'symbol', value: ctx.fn }
|
|
474
|
-
case 'call_arg':
|
|
475
|
-
return ctx.args[leaf.index]
|
|
476
|
-
case 'call_arg_len':
|
|
477
|
-
// No direct ScVal projection: the length is an integer the comparator
|
|
478
|
-
// resolves against the right-hand literal. Returning undefined keeps
|
|
479
|
-
// the `in` membership path structurally informed (no haystack match).
|
|
480
|
-
return undefined
|
|
481
|
-
case 'call_arg_field': {
|
|
482
|
-
const actual = ctx.args[leaf.index]
|
|
483
|
-
if (actual?.type !== 'vec') return undefined
|
|
484
|
-
const element = actual.value[leaf.element]
|
|
485
|
-
if (element?.type !== 'map') return undefined
|
|
486
|
-
if (!Array.isArray(element.value)) return undefined
|
|
487
|
-
const entry = element.value.find((e) => e.key === leaf.field)
|
|
488
|
-
return entry ? entry.val : undefined
|
|
489
|
-
}
|
|
490
|
-
case 'amount':
|
|
491
|
-
case 'window_spent':
|
|
492
|
-
case 'oracle_price':
|
|
493
|
-
case 'invocation_count_in_window':
|
|
494
|
-
case 'now':
|
|
495
|
-
case 'valid_until':
|
|
496
|
-
case 'call_arg_scaled':
|
|
497
|
-
return undefined // selector leaves with no ScVal projection
|
|
498
|
-
case 'literal_address':
|
|
499
|
-
return { type: 'address', value: leaf.value }
|
|
500
|
-
case 'literal_i128':
|
|
501
|
-
return { type: 'i128', value: leaf.value }
|
|
502
|
-
case 'literal_symbol':
|
|
503
|
-
return { type: 'symbol', value: leaf.value }
|
|
504
|
-
case 'literal_u32':
|
|
505
|
-
return { type: 'u32', value: String(leaf.value) }
|
|
506
|
-
case 'literal_u64':
|
|
507
|
-
return { type: 'u64', value: leaf.value }
|
|
508
|
-
case 'literal_bytes':
|
|
509
|
-
return { type: 'bytes', value: leaf.value }
|
|
510
|
-
case 'literal_vec':
|
|
511
|
-
return undefined
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/** Step 6: amount / window_spent compare on BigInt. Both leaves compare a
|
|
516
|
-
* per-token BigInt record (current vs. rolling) to an i128 literal; the
|
|
517
|
-
* compare + denial reason are identical, so a single helper handles both. */
|
|
518
|
-
function evalAmountCompare(
|
|
519
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
520
|
-
token: string,
|
|
521
|
-
right: PredicateLeaf,
|
|
522
|
-
ctx: EvalContext,
|
|
523
|
-
record: 'amountByToken' | 'windowSpentByToken'
|
|
524
|
-
): EvalResult {
|
|
525
|
-
const literal = right.kind === 'literal_i128' ? right.value : null
|
|
526
|
-
if (literal === null) return { permit: false, reason: 'AMOUNT_BOUND' }
|
|
527
|
-
const actual = ctx[record][token] ?? '0'
|
|
528
|
-
return bigintCmp(op, actual, literal)
|
|
529
|
-
? { permit: true }
|
|
530
|
-
: { permit: false, reason: 'AMOUNT_BOUND' }
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/** Step 6: window_spent compare on BigInt. */
|
|
534
|
-
function evalWindowSpentCompare(
|
|
535
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
536
|
-
token: string,
|
|
537
|
-
right: PredicateLeaf,
|
|
538
|
-
ctx: EvalContext
|
|
539
|
-
): EvalResult {
|
|
540
|
-
return evalAmountCompare(op, token, right, ctx, 'windowSpentByToken')
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/** Step 7: invocation_count_in_window compare. */
|
|
544
|
-
function evalFrequencyCompare(
|
|
545
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
546
|
-
windowSecs: number,
|
|
547
|
-
right: PredicateLeaf,
|
|
548
|
-
ctx: EvalContext
|
|
549
|
-
): EvalResult {
|
|
550
|
-
const literal = right.kind === 'literal_u32' ? String(right.value) : null
|
|
551
|
-
if (literal === null) return { permit: false, reason: 'FREQUENCY' }
|
|
552
|
-
const actual = String(ctx.invocationCountByWindow[windowSecs] ?? 0)
|
|
553
|
-
return bigintCmp(op, actual, literal) ? { permit: true } : { permit: false, reason: 'FREQUENCY' }
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/** Step 8: oracle_price compare. Reads `ctx.oraclePriceByAsset[asset]`. Any
|
|
557
|
-
* error entry OR a missing key throws `OracleError` (FATAL). A satisfied
|
|
558
|
-
* compare permits. */
|
|
559
|
-
function evalOracleCompare(
|
|
560
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
561
|
-
asset: string,
|
|
562
|
-
right: PredicateLeaf,
|
|
563
|
-
ctx: EvalContext
|
|
564
|
-
): EvalResult {
|
|
565
|
-
const entry = ctx.oraclePriceByAsset[asset]
|
|
566
|
-
if (!entry) throw new OracleError('ORACLE_STALE')
|
|
567
|
-
if ('error' in entry) {
|
|
568
|
-
const mapped = ORACLE_ERROR_CODES[entry.error]
|
|
569
|
-
if (!mapped) throw new OracleError('ORACLE_STALE')
|
|
570
|
-
throw new OracleError(mapped)
|
|
571
|
-
}
|
|
572
|
-
// Mirrors eval_oracle_compare in dsl.rs. The threshold MUST declare its
|
|
573
|
-
// decimal basis: prices are on the normalised 9-dp basis, and a bare literal
|
|
574
|
-
// is refused (not assumed) because a raw 14-dp threshold that assumed the
|
|
575
|
-
// 9-dp basis would permit everything.
|
|
576
|
-
if (right.kind !== 'oracle_threshold') throw new OracleError('ORACLE_DECIMALS_MISMATCH')
|
|
577
|
-
if (right.decimals > MAX_ORACLE_THRESHOLD_DECIMALS) {
|
|
578
|
-
throw new OracleError('ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE')
|
|
579
|
-
}
|
|
580
|
-
// Scale BOTH sides up to the wider basis; dividing the threshold down
|
|
581
|
-
// truncates and moves the permit boundary.
|
|
582
|
-
const decimals = BigInt(right.decimals)
|
|
583
|
-
const normalised = BigInt(NORMALISED_DECIMALS)
|
|
584
|
-
const common = decimals > normalised ? decimals : normalised
|
|
585
|
-
const priceScaled = BigInt(entry.price) * 10n ** (common - normalised)
|
|
586
|
-
const literalScaled = BigInt(right.value) * 10n ** (common - decimals)
|
|
587
|
-
// A violated oracle threshold is a stateful bound (Rust dsl.rs:276 returns
|
|
588
|
-
// `DenyReason::StatefulBound`, deny code #104). The TS evaluator must mirror
|
|
589
|
-
// the same reason so a future TS/Rust reason divergence fails CI; the
|
|
590
|
-
// cross-layer harness in this file now asserts reason codes match.
|
|
591
|
-
return bigintCmp(op, String(priceScaled), String(literalScaled))
|
|
592
|
-
? { permit: true }
|
|
593
|
-
: { permit: false, reason: 'STATEFUL_BOUND' }
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
|
|
597
|
-
* equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
|
|
598
|
-
function bigintCmp(op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte', aStr: string, bStr: string): boolean {
|
|
599
|
-
const a = BigInt(aStr)
|
|
600
|
-
const b = BigInt(bStr)
|
|
601
|
-
switch (op) {
|
|
602
|
-
case 'eq':
|
|
603
|
-
return a === b
|
|
604
|
-
case 'lt':
|
|
605
|
-
return a < b
|
|
606
|
-
case 'lte':
|
|
607
|
-
return a <= b
|
|
608
|
-
case 'gt':
|
|
609
|
-
return a > b
|
|
610
|
-
case 'gte':
|
|
611
|
-
return a >= b
|
|
612
|
-
}
|
|
613
|
-
}
|
package/src/synth/harness.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { PredicateNode } from '../types.ts'
|
|
2
|
-
import type { GeneratedCases } from './deny-cases.ts'
|
|
3
|
-
import { evaluate } from './evaluate.ts'
|
|
4
|
-
|
|
5
|
-
export interface HarnessFailure {
|
|
6
|
-
dimension: string
|
|
7
|
-
expected: 'permit' | 'deny'
|
|
8
|
-
got: 'permit' | 'deny'
|
|
9
|
-
reason?: string
|
|
10
|
-
/** When `expectedReason` is set on the deny case and the case actually
|
|
11
|
-
* denies, this carries the canonical reason the TS evaluator MUST emit
|
|
12
|
-
* to stay in lockstep with the Rust interpreter (the cross-layer
|
|
13
|
-
* reason-code contract). A future TS/Rust reason divergence surfaces
|
|
14
|
-
* here in CI. */
|
|
15
|
-
expectedReason?: string
|
|
16
|
-
actualReason?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type HarnessResult = { ok: true } | { ok: false; failures: HarnessFailure[] }
|
|
20
|
-
|
|
21
|
-
/** Evaluate the intended call and every generated denial without side effects.
|
|
22
|
-
* Asserts both the boolean decision AND, for deny cases that carry an
|
|
23
|
-
* `expectedReason`, the concrete reason the TS evaluator emits. The reason
|
|
24
|
-
* is what the Rust interpreter returns; the cross-layer conformance test in
|
|
25
|
-
* `contracts/policy-interpreter/tests/conformance` mirrors the same contract
|
|
26
|
-
* for the verifyLive path. */
|
|
27
|
-
export function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult {
|
|
28
|
-
const failures: HarnessFailure[] = []
|
|
29
|
-
const permitResult = evaluate(predicate, cases.permit)
|
|
30
|
-
if (!permitResult.permit) {
|
|
31
|
-
failures.push({
|
|
32
|
-
dimension: 'PERMIT_CASE_FAILED',
|
|
33
|
-
expected: 'permit',
|
|
34
|
-
got: 'deny',
|
|
35
|
-
reason: permitResult.reason,
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (const deny of cases.denies) {
|
|
40
|
-
const result = evaluate(predicate, deny.ctx)
|
|
41
|
-
if (result.permit) {
|
|
42
|
-
failures.push({
|
|
43
|
-
dimension: deny.dimension,
|
|
44
|
-
expected: 'deny',
|
|
45
|
-
got: 'permit',
|
|
46
|
-
reason: 'DENY_CASE_FAILURE',
|
|
47
|
-
})
|
|
48
|
-
continue
|
|
49
|
-
}
|
|
50
|
-
// Reason-code assertion (cross-layer contract). When a deny case carries
|
|
51
|
-
// an expected reason, the TS evaluator MUST emit that exact string; a
|
|
52
|
-
// divergence means the TS model and the Rust interpreter disagree on
|
|
53
|
-
// what the user sees in CI - the harness fails so the diff is visible
|
|
54
|
-
// here, not at runtime in production.
|
|
55
|
-
if (deny.expectedReason !== undefined && result.reason !== deny.expectedReason) {
|
|
56
|
-
failures.push({
|
|
57
|
-
dimension: deny.dimension,
|
|
58
|
-
expected: 'deny',
|
|
59
|
-
got: 'deny',
|
|
60
|
-
reason: 'REASON_MISMATCH',
|
|
61
|
-
expectedReason: deny.expectedReason,
|
|
62
|
-
actualReason: result.reason,
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return failures.length === 0 ? { ok: true } : { ok: false, failures }
|
|
68
|
-
}
|