@dcasegr/amm-client 0.0.0-stage → 0.38.2
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 +107 -2
- package/dist/client.d.ts +534 -0
- package/dist/client.js +1319 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +53 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.js +64 -0
- package/dist/lib/keys.d.ts +42 -0
- package/dist/lib/keys.js +48 -0
- package/dist/lib/overlay.d.ts +190 -0
- package/dist/lib/overlay.js +302 -0
- package/dist/lib/skein.d.ts +82 -0
- package/dist/lib/skein.js +142 -0
- package/dist/lib/tokenId.d.ts +72 -0
- package/dist/lib/tokenId.js +121 -0
- package/dist/lp/amounts.d.ts +12 -0
- package/dist/lp/amounts.js +38 -0
- package/dist/lp/close.d.ts +90 -0
- package/dist/lp/close.js +140 -0
- package/dist/lp/images.d.ts +11 -0
- package/dist/lp/images.js +45 -0
- package/dist/lp/inventory.d.ts +68 -0
- package/dist/lp/inventory.js +84 -0
- package/dist/lp/poolDeploy.d.ts +220 -0
- package/dist/lp/poolDeploy.js +291 -0
- package/dist/lp/poolRows.d.ts +13 -0
- package/dist/lp/poolRows.js +19 -0
- package/dist/lp/positions.d.ts +81 -0
- package/dist/lp/positions.js +144 -0
- package/dist/lp/rebalance.d.ts +101 -0
- package/dist/lp/rebalance.js +168 -0
- package/dist/lp/wallet.d.ts +12 -0
- package/dist/lp/wallet.js +19 -0
- package/dist/market/activeSwap.d.ts +158 -0
- package/dist/market/activeSwap.js +320 -0
- package/dist/market/plan.d.ts +85 -0
- package/dist/market/plan.js +149 -0
- package/dist/market/prices.d.ts +13 -0
- package/dist/market/prices.js +14 -0
- package/dist/market/relay.d.ts +84 -0
- package/dist/market/relay.js +201 -0
- package/dist/market/swapAction.d.ts +306 -0
- package/dist/market/swapAction.js +508 -0
- package/dist/market/swapFlow.d.ts +58 -0
- package/dist/market/swapFlow.js +86 -0
- package/dist/market/view.d.ts +58 -0
- package/dist/market/view.js +63 -0
- package/dist/pool/artifact.d.ts +23 -0
- package/dist/pool/artifact.js +38 -0
- package/dist/pool/index.d.ts +8 -0
- package/dist/pool/index.js +8 -0
- package/dist/pool/oneColor.d.ts +19 -0
- package/dist/pool/oneColor.js +39 -0
- package/dist/pool/pool.artifact.json +5069 -0
- package/dist/pool/runar-ir-schema.d.ts +21 -0
- package/dist/pool/runar-ir-schema.js +18 -0
- package/dist/pool/template.d.ts +295 -0
- package/dist/pool/template.js +715 -0
- package/dist/vendor/runar/LICENSE +21 -0
- package/dist/vendor/runar/NOTICE +15 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
- package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
- package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
- package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
- package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
- package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
- package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
- package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
- package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
- package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
- package/dist/vendor/runar/runar-sdk.d.ts +9 -0
- package/dist/vendor/runar/runar-sdk.js +8 -0
- package/dist/wallet/action.d.ts +28 -0
- package/dist/wallet/action.js +42 -0
- package/dist/wallet/brc29.d.ts +83 -0
- package/dist/wallet/brc29.js +80 -0
- package/dist/wallet/color.d.ts +110 -0
- package/dist/wallet/color.js +134 -0
- package/dist/wallet/swapPayouts.d.ts +35 -0
- package/dist/wallet/swapPayouts.js +146 -0
- package/package.json +39 -4
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight ANF interpreter for auto-computing state transitions.
|
|
3
|
+
*
|
|
4
|
+
* Given a compiled artifact's ANF IR, the current contract state, and
|
|
5
|
+
* method arguments, this interpreter walks the ANF bindings and computes
|
|
6
|
+
* the new state. It handles `update_prop` nodes to track state mutations,
|
|
7
|
+
* while skipping on-chain-only operations like `check_preimage`,
|
|
8
|
+
* `deserialize_state`, `get_state_script`, `add_output`, and `add_raw_output`.
|
|
9
|
+
*
|
|
10
|
+
* Three execution modes:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Lenient** (`computeNewState`, `computeNewStateAndDataOutputs`) —
|
|
13
|
+
* skips `assert` predicates so the SDK can pre-compute the post-state
|
|
14
|
+
* even when arguments wouldn't actually pass the on-chain script. This
|
|
15
|
+
* is the canonical use: auto-deriving `newState` for the next call.
|
|
16
|
+
* 2. **Strict** (`executeStrict`) — evaluates every `assert` predicate and
|
|
17
|
+
* throws `AssertionFailureError` (carrying the contract method + ANF
|
|
18
|
+
* binding name of the failing predicate) on the first false assert. Use
|
|
19
|
+
* for "will this call go through?" / "would the asserts hold" smoke
|
|
20
|
+
* tests off-chain before paying broadcast fees. Crypto built-ins
|
|
21
|
+
* (`checkSig`, `checkMultiSig`, `checkPreimage`) still mock-return
|
|
22
|
+
* `true` — only explicit `assert(...)` predicates are enforced.
|
|
23
|
+
* 3. **On-chain authoritative** (`executeOnChainAuthoritative`) — strict
|
|
24
|
+
* assert enforcement PLUS real ECDSA / SHA-256 preimage verification
|
|
25
|
+
* against a caller-supplied `sighash`. The signature shape requires the
|
|
26
|
+
* caller to provide the sighash up front, so it is impossible to invoke
|
|
27
|
+
* this mode accidentally without the cryptographic inputs. Use this to
|
|
28
|
+
* validate the exact transaction the caller intends to broadcast: a
|
|
29
|
+
* `checkSig(sig, pk)` only passes if the supplied DER signature
|
|
30
|
+
* verifies against the supplied compressed public key over the
|
|
31
|
+
* supplied sighash, and `checkPreimage(preimage)` only passes if
|
|
32
|
+
* `SHA256(SHA256(preimage)) === sighash` (the canonical BIP-143
|
|
33
|
+
* `OP_PUSH_TX` semantic).
|
|
34
|
+
*/
|
|
35
|
+
import { Hash, Utils, PublicKey, Signature, BigNumber } from '@bsv/sdk';
|
|
36
|
+
// `verify` from @bsv/sdk's ECDSA module performs raw ECDSA verification
|
|
37
|
+
// against an already-hashed digest. We use this rather than
|
|
38
|
+
// `pubKey.verify(...)` (which internally sha256s its first arg) so the
|
|
39
|
+
// `sighash` passed by the caller is treated as the actual ECDSA digest,
|
|
40
|
+
// matching the on-chain CHECKSIG semantic where ECDSA verifies against
|
|
41
|
+
// the BIP-143 sighash directly with no extra hashing.
|
|
42
|
+
import { ECDSA } from '@bsv/sdk';
|
|
43
|
+
const ecdsaVerifyRaw = ECDSA.verify;
|
|
44
|
+
import { decodeScriptNumber } from './script-utils.js';
|
|
45
|
+
export function computeNewState(anf, methodName, currentState, args, constructorArgs = []) {
|
|
46
|
+
return computeNewStateAndDataOutputs(anf, methodName, currentState, args, constructorArgs).state;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Like {@link computeNewState} but also returns data outputs resolved
|
|
50
|
+
* from `this.addDataOutput(...)` and raw outputs resolved from
|
|
51
|
+
* `this.addRawOutput(...)` in the method body. Entries appear in
|
|
52
|
+
* declaration order and are what `buildCallTransaction` should emit
|
|
53
|
+
* between state outputs and the change output so the on-chain
|
|
54
|
+
* continuation-hash check passes.
|
|
55
|
+
*/
|
|
56
|
+
export function computeNewStateAndDataOutputs(anf, methodName, currentState, args, constructorArgs = []) {
|
|
57
|
+
return runMethod(anf, methodName, currentState, args, constructorArgs, null);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Thrown by {@link executeStrict} on the first failing `assert` predicate.
|
|
61
|
+
* Carries enough context to point a developer at the exact ANF binding
|
|
62
|
+
* that aborted: the method being executed and the binding name (e.g.
|
|
63
|
+
* `assertPositive` from the source `assert(amount > 0)`).
|
|
64
|
+
*/
|
|
65
|
+
export class AssertionFailureError extends Error {
|
|
66
|
+
methodName;
|
|
67
|
+
bindingName;
|
|
68
|
+
constructor(methodName, bindingName) {
|
|
69
|
+
super(`assert failed in ${methodName}: binding '${bindingName}' evaluated to false`);
|
|
70
|
+
this.name = 'AssertionFailureError';
|
|
71
|
+
this.methodName = methodName;
|
|
72
|
+
this.bindingName = bindingName;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Strict-mode counterpart to {@link computeNewStateAndDataOutputs}: walks
|
|
77
|
+
* the same ANF body but throws {@link AssertionFailureError} on the first
|
|
78
|
+
* `assert(predicate)` whose predicate evaluates to a falsy value. Use this
|
|
79
|
+
* before broadcasting a transaction to surface guard failures off-chain
|
|
80
|
+
* instead of relying on a node rejection. Crypto built-ins (`checkSig`,
|
|
81
|
+
* `checkMultiSig`, `checkPreimage`) still mock-return `true` — strict mode
|
|
82
|
+
* only enforces explicit `assert(...)` predicates.
|
|
83
|
+
*/
|
|
84
|
+
export function executeStrict(anf, methodName, currentState, args, constructorArgs = []) {
|
|
85
|
+
return runMethod(anf, methodName, currentState, args, constructorArgs, {
|
|
86
|
+
methodName,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Like {@link executeStrict} but also performs real cryptographic
|
|
91
|
+
* verification of `checkSig`, `checkMultiSig`, and `checkPreimage` against
|
|
92
|
+
* the supplied `sighash`. Throws {@link AssertionFailureError} when any
|
|
93
|
+
* `assert(...)` (including the implicit one wrapping a failed crypto
|
|
94
|
+
* built-in) fires.
|
|
95
|
+
*
|
|
96
|
+
* The `ctx` parameter is mandatory and carries the sighash, so it is
|
|
97
|
+
* impossible to call this entry point accidentally without supplying the
|
|
98
|
+
* cryptographic inputs the verification needs.
|
|
99
|
+
*/
|
|
100
|
+
export function executeOnChainAuthoritative(anf, methodName, currentState, args, constructorArgs, ctx) {
|
|
101
|
+
const sighash = normalizeSighash(ctx.sighash);
|
|
102
|
+
return runMethod(anf, methodName, currentState, args, constructorArgs, {
|
|
103
|
+
methodName,
|
|
104
|
+
realCrypto: { sighash },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function normalizeSighash(sighash) {
|
|
108
|
+
let bytes;
|
|
109
|
+
if (typeof sighash === 'string') {
|
|
110
|
+
bytes = Utils.toArray(sighash, 'hex');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
bytes = Array.from(sighash);
|
|
114
|
+
}
|
|
115
|
+
if (bytes.length !== 32) {
|
|
116
|
+
throw new Error(`executeOnChainAuthoritative: sighash must be exactly 32 bytes, got ${bytes.length}`);
|
|
117
|
+
}
|
|
118
|
+
return bytes;
|
|
119
|
+
}
|
|
120
|
+
function runMethod(anf, methodName, currentState, args, constructorArgs, strict) {
|
|
121
|
+
// Find the method in ANF
|
|
122
|
+
const method = anf.methods.find((m) => m.name === methodName && m.isPublic);
|
|
123
|
+
if (!method) {
|
|
124
|
+
throw new Error(`computeNewState: method '${methodName}' not found in ANF IR`);
|
|
125
|
+
}
|
|
126
|
+
// Initialize the environment with property values and method params
|
|
127
|
+
const env = {};
|
|
128
|
+
// Load properties: mutable fields from currentState, readonly fields
|
|
129
|
+
// from constructorArgs (matched by constructor param index, which excludes
|
|
130
|
+
// initialized properties).
|
|
131
|
+
// Build the constructor param index: position among non-initialized properties.
|
|
132
|
+
const ctorParamNames = anf.properties
|
|
133
|
+
.filter((p) => p.initialValue === undefined)
|
|
134
|
+
.map((p) => p.name);
|
|
135
|
+
for (const prop of anf.properties) {
|
|
136
|
+
if (prop.name in currentState) {
|
|
137
|
+
env[prop.name] = currentState[prop.name];
|
|
138
|
+
}
|
|
139
|
+
else if (prop.initialValue !== undefined) {
|
|
140
|
+
env[prop.name] = prop.initialValue;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const ctorIdx = ctorParamNames.indexOf(prop.name);
|
|
144
|
+
if (ctorIdx >= 0 && ctorIdx < constructorArgs.length) {
|
|
145
|
+
env[prop.name] = constructorArgs[ctorIdx];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Load method params (skip implicit ones injected by the compiler)
|
|
150
|
+
const implicitParams = new Set([
|
|
151
|
+
'_changePKH', '_changeAmount', '_newAmount', 'txPreimage',
|
|
152
|
+
]);
|
|
153
|
+
for (const param of method.params) {
|
|
154
|
+
if (implicitParams.has(param.name))
|
|
155
|
+
continue;
|
|
156
|
+
if (param.name in args) {
|
|
157
|
+
env[param.name] = args[param.name];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Track state mutations, data outputs, and raw outputs.
|
|
161
|
+
// `rawOutputs` holds entries from `add_raw_output` ANF kinds, which the
|
|
162
|
+
// simulator does NOT introspect (the script is caller-supplied). They
|
|
163
|
+
// are surfaced in the result envelope so an off-chain transaction
|
|
164
|
+
// builder can splice them in at the correct index.
|
|
165
|
+
const stateDelta = {};
|
|
166
|
+
const dataOutputs = [];
|
|
167
|
+
const rawOutputs = [];
|
|
168
|
+
// Ordered state-class outputs (state continuation + raw) in source order.
|
|
169
|
+
const outputs = [];
|
|
170
|
+
// Walk bindings
|
|
171
|
+
evalBindings(method.body, env, stateDelta, dataOutputs, rawOutputs, outputs, anf, strict);
|
|
172
|
+
return { state: { ...currentState, ...stateDelta }, dataOutputs, rawOutputs, outputs };
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Binding evaluation
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
function evalBindings(bindings, env, stateDelta, dataOutputs, rawOutputs, outputs, anf, strict = null,
|
|
178
|
+
// Per-binding raw stack bytes for byte-array-op results (& | ^ << >> ~). Keyed
|
|
179
|
+
// by binding name; lets a chained op read the real (possibly non-minimal)
|
|
180
|
+
// length of a prior op's result instead of re-minimizing its numeric value.
|
|
181
|
+
// `env` stays pure (decoded values) so state serialization is unaffected.
|
|
182
|
+
scriptBytes = {}) {
|
|
183
|
+
for (const binding of bindings) {
|
|
184
|
+
const val = evalValue(binding.value, env, stateDelta, dataOutputs, rawOutputs, outputs, anf, strict, binding.name, scriptBytes);
|
|
185
|
+
env[binding.name] = val;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function evalValue(value, env, stateDelta, dataOutputs, rawOutputs, outputs, anf, strict = null, bindingName = '<anonymous>', scriptBytes = {}) {
|
|
189
|
+
switch (value.kind) {
|
|
190
|
+
case 'load_param':
|
|
191
|
+
return env[value.name];
|
|
192
|
+
case 'load_prop':
|
|
193
|
+
return env[value.name];
|
|
194
|
+
case 'load_const': {
|
|
195
|
+
const v = value.value;
|
|
196
|
+
// Handle @ref: aliases (load_const with "@ref:targetName")
|
|
197
|
+
if (typeof v === 'string' && v.startsWith('@ref:')) {
|
|
198
|
+
const target = v.slice(5);
|
|
199
|
+
aliasScriptBytes(scriptBytes, target, bindingName);
|
|
200
|
+
return env[target];
|
|
201
|
+
}
|
|
202
|
+
// On-disk ANF spells every bigint as a `"<decimal>n"` STRING (see
|
|
203
|
+
// `jsonWithBigInt` in runar-cli's compile command) — that is the artifact
|
|
204
|
+
// every SDK loads with a bare `JSON.parse`. Decode it here so a const
|
|
205
|
+
// operand is a `bigint`, not a `string`: the byte-op paths below gate on
|
|
206
|
+
// `typeof !== 'string'`, so leaving it a string silently routes `<< >> &
|
|
207
|
+
// | ^ ~` down the ByteString branch and the SDK builds a continuation the
|
|
208
|
+
// deployed script disagrees with (NEW-008). Go / Rust / Zig already
|
|
209
|
+
// decode this shape; this makes all seven agree with the script.
|
|
210
|
+
//
|
|
211
|
+
// Unambiguous: ANF ByteString literals are hex and `n` is not a hex
|
|
212
|
+
// digit, so `^-?\d+n$` cannot be a bytestring.
|
|
213
|
+
if (typeof v === 'string' && /^-?\d+n$/.test(v)) {
|
|
214
|
+
return BigInt(v.slice(0, -1));
|
|
215
|
+
}
|
|
216
|
+
return v;
|
|
217
|
+
}
|
|
218
|
+
case 'bin_op': {
|
|
219
|
+
// Numeric byte-array ops (& | ^ << >>) thread the operands' real stack
|
|
220
|
+
// bytes so chained expressions match the deployed script (a shift/bitwise
|
|
221
|
+
// result can be non-minimal; the next length-sensitive op must see that).
|
|
222
|
+
// ByteString ops (result_type 'bytes' / string operands) fall through to
|
|
223
|
+
// evalBinOp, which keeps the minimal-operand path for everything else.
|
|
224
|
+
const isNumericByteOp = (value.op === '&' || value.op === '|' || value.op === '^' ||
|
|
225
|
+
value.op === '<<' || value.op === '>>') &&
|
|
226
|
+
value.result_type !== 'bytes' &&
|
|
227
|
+
typeof env[value.left] !== 'string' &&
|
|
228
|
+
typeof env[value.right] !== 'string';
|
|
229
|
+
if (isNumericByteOp) {
|
|
230
|
+
const ab = scriptBytes[value.left] ?? minimalScriptNumberBytes(toBigInt(env[value.left]));
|
|
231
|
+
let rb;
|
|
232
|
+
if (value.op === '<<' || value.op === '>>') {
|
|
233
|
+
// Shift count is read as a number on-chain — only `ab`'s length matters.
|
|
234
|
+
rb = scriptNumberShiftBytes(value.op, ab, toBigInt(env[value.right]));
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const bb = scriptBytes[value.right] ?? minimalScriptNumberBytes(toBigInt(env[value.right]));
|
|
238
|
+
rb = scriptNumberBitwiseBytes(value.op, ab, bb);
|
|
239
|
+
}
|
|
240
|
+
scriptBytes[bindingName] = rb;
|
|
241
|
+
return decodeScriptNumber(Utils.toHex(rb));
|
|
242
|
+
}
|
|
243
|
+
// NUMERIC consumption: a node decodes with fRequireMinimal=true and
|
|
244
|
+
// ABORTS on a non-minimal operand. Only the byte-array ops above may
|
|
245
|
+
// take one. `&& !isNumericByteOp` is implicit — we only reach here when
|
|
246
|
+
// the op is not one of them.
|
|
247
|
+
assertMinimalNumericOperand(scriptBytes[value.left], env[value.left], value.op);
|
|
248
|
+
assertMinimalNumericOperand(scriptBytes[value.right], env[value.right], value.op);
|
|
249
|
+
return evalBinOp(value.op, env[value.left], env[value.right], value.result_type);
|
|
250
|
+
}
|
|
251
|
+
case 'unary_op': {
|
|
252
|
+
if (value.op === '~' && value.result_type !== 'bytes' && typeof env[value.operand] !== 'string') {
|
|
253
|
+
const ab = scriptBytes[value.operand] ?? minimalScriptNumberBytes(toBigInt(env[value.operand]));
|
|
254
|
+
const rb = scriptNumberInvertBytes(ab);
|
|
255
|
+
scriptBytes[bindingName] = rb;
|
|
256
|
+
return decodeScriptNumber(Utils.toHex(rb));
|
|
257
|
+
}
|
|
258
|
+
assertMinimalNumericOperand(scriptBytes[value.operand], env[value.operand], value.op);
|
|
259
|
+
return evalUnaryOp(value.op, env[value.operand], value.result_type);
|
|
260
|
+
}
|
|
261
|
+
case 'call': {
|
|
262
|
+
// Strict mode: a `call(assert, x)` lowering path must enforce the
|
|
263
|
+
// predicate the same way the dedicated `assert` ANF node does.
|
|
264
|
+
if (strict && value.func === 'assert') {
|
|
265
|
+
const arg = env[value.args[0] ?? ''];
|
|
266
|
+
if (!isTruthy(arg)) {
|
|
267
|
+
throw new AssertionFailureError(strict.methodName, bindingName);
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return evalCall(value.func, value.args.map((a) => env[a]), strict?.realCrypto);
|
|
272
|
+
}
|
|
273
|
+
case 'method_call':
|
|
274
|
+
return evalMethodCall(env, value.method, value.args.map((a) => env[a]), stateDelta, dataOutputs, rawOutputs, outputs, anf);
|
|
275
|
+
case 'if': {
|
|
276
|
+
const cond = env[value.cond];
|
|
277
|
+
const branch = isTruthy(cond) ? value.then : value.else;
|
|
278
|
+
// Create a child env for the branch
|
|
279
|
+
const childEnv = { ...env };
|
|
280
|
+
evalBindings(branch, childEnv, stateDelta, dataOutputs, rawOutputs, outputs, anf, strict, scriptBytes);
|
|
281
|
+
// Copy any new bindings back (the last binding is typically the branch result)
|
|
282
|
+
Object.assign(env, childEnv);
|
|
283
|
+
// Return the last binding's value from the branch
|
|
284
|
+
if (branch.length > 0) {
|
|
285
|
+
const lastName = branch[branch.length - 1].name;
|
|
286
|
+
aliasScriptBytes(scriptBytes, lastName, bindingName);
|
|
287
|
+
return childEnv[lastName];
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
case 'loop': {
|
|
292
|
+
const { count, body, iterVar } = value;
|
|
293
|
+
// Iteration `i` binds `iterVar = start + i*step` (issue #121). Older ANF
|
|
294
|
+
// payloads without start/step describe zero-start counting-up loops.
|
|
295
|
+
const start = value.start ?? 0n;
|
|
296
|
+
const step = BigInt(value.step ?? 1);
|
|
297
|
+
let lastVal;
|
|
298
|
+
for (let i = 0; i < count; i++) {
|
|
299
|
+
env[iterVar] = start + BigInt(i) * step;
|
|
300
|
+
const loopEnv = { ...env };
|
|
301
|
+
evalBindings(body, loopEnv, stateDelta, dataOutputs, rawOutputs, outputs, anf, strict, scriptBytes);
|
|
302
|
+
// Copy loop bindings back
|
|
303
|
+
Object.assign(env, loopEnv);
|
|
304
|
+
if (body.length > 0) {
|
|
305
|
+
const lastName = body[body.length - 1].name;
|
|
306
|
+
aliasScriptBytes(scriptBytes, lastName, bindingName);
|
|
307
|
+
lastVal = loopEnv[lastName];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return lastVal;
|
|
311
|
+
}
|
|
312
|
+
case 'assert': {
|
|
313
|
+
if (strict) {
|
|
314
|
+
// Marker-based skip: the auto-injected stateful-continuation
|
|
315
|
+
// `assert(hash256(_) === extractOutputHash(_))` carries
|
|
316
|
+
// `isAutoInjectedStateCheck: true` (set in
|
|
317
|
+
// `packages/runar-compiler/src/passes/04-anf-lower.ts`). The
|
|
318
|
+
// on-chain VM is authoritative for that check; off-chain we
|
|
319
|
+
// have no realistic continuation hash. Developer-written
|
|
320
|
+
// covenant asserts with the identical IR shape carry no
|
|
321
|
+
// marker and ARE enforced (see BUG-002).
|
|
322
|
+
if (value.isAutoInjectedStateCheck === true) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const predicate = env[value.value];
|
|
326
|
+
if (!isTruthy(predicate)) {
|
|
327
|
+
throw new AssertionFailureError(strict.methodName, bindingName);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Lenient: skip asserts (the on-chain script handles enforcement)
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
case 'update_prop': {
|
|
334
|
+
const newVal = env[value.value];
|
|
335
|
+
env[value.name] = newVal;
|
|
336
|
+
stateDelta[value.name] = newVal;
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
case 'add_output': {
|
|
340
|
+
// Extract implicit state changes from stateValues array.
|
|
341
|
+
// stateValues[i] maps to the i-th mutable property (declaration order).
|
|
342
|
+
if (anf && value.stateValues && value.stateValues.length > 0) {
|
|
343
|
+
const mutableProps = anf.properties.filter((p) => !p.readonly);
|
|
344
|
+
for (let i = 0; i < value.stateValues.length && i < mutableProps.length; i++) {
|
|
345
|
+
const propName = mutableProps[i].name;
|
|
346
|
+
const ref = value.stateValues[i];
|
|
347
|
+
const newVal = env[ref];
|
|
348
|
+
env[propName] = newVal;
|
|
349
|
+
stateDelta[propName] = newVal;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Record the state continuation output in source order (finding G1): a
|
|
353
|
+
// method may interleave raw outputs around it, and the on-chain covenant
|
|
354
|
+
// folds them into hashOutputs in exactly this order.
|
|
355
|
+
outputs.push({ kind: 'state', satoshis: toBigInt(env[value.satoshis]) });
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
case 'add_data_output': {
|
|
359
|
+
// Resolve the two arg refs from env and record the data output.
|
|
360
|
+
const sats = toBigInt(env[value.satoshis]);
|
|
361
|
+
const script = env[value.scriptBytes];
|
|
362
|
+
dataOutputs.push({
|
|
363
|
+
satoshis: sats,
|
|
364
|
+
script: typeof script === 'string' ? script : '',
|
|
365
|
+
});
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
case 'add_raw_output': {
|
|
369
|
+
// `addRawOutput(satoshis, scriptBytes)`. The simulator does not
|
|
370
|
+
// introspect the script bytes (they're caller-supplied raw locking
|
|
371
|
+
// script); it simply forwards them in the result envelope so an
|
|
372
|
+
// off-chain transaction builder can emit the output at the correct
|
|
373
|
+
// index. Crypto built-ins remain mocked even in strict mode.
|
|
374
|
+
const sats = toBigInt(env[value.satoshis]);
|
|
375
|
+
const script = env[value.scriptBytes];
|
|
376
|
+
rawOutputs.push({
|
|
377
|
+
satoshis: sats,
|
|
378
|
+
script: typeof script === 'string' ? script : '',
|
|
379
|
+
});
|
|
380
|
+
// Also record in the ordered state-class output list so a transaction
|
|
381
|
+
// builder can emit it at the correct source-order index (finding G1).
|
|
382
|
+
outputs.push({
|
|
383
|
+
kind: 'raw',
|
|
384
|
+
satoshis: sats,
|
|
385
|
+
script: typeof script === 'string' ? script : '',
|
|
386
|
+
});
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
// On-chain-only operations — skip in simulation. These ANF kinds are
|
|
390
|
+
// markers consumed by the codegen, not by the off-chain interpreter.
|
|
391
|
+
case 'check_preimage':
|
|
392
|
+
case 'deserialize_state':
|
|
393
|
+
case 'get_state_script':
|
|
394
|
+
return undefined;
|
|
395
|
+
default:
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Script-number bitwise / shift semantics (byte-array ops, NOT numeric)
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
//
|
|
403
|
+
// OP_AND/OP_OR/OP_XOR/OP_INVERT/OP_LSHIFT/OP_RSHIFT operate on the RAW BYTES
|
|
404
|
+
// of the operands' minimal script-number encoding, not on their numeric
|
|
405
|
+
// value (spec/opcodes.md). AND/OR/XOR require equal-length operands and
|
|
406
|
+
// fail otherwise; shifts treat the byte array as a big-endian bit string
|
|
407
|
+
// and preserve its length. This reproduces EXACTLY what
|
|
408
|
+
// packages/runar-testing/src/vm/utils.ts's
|
|
409
|
+
// scriptNumberBitwise/scriptNumberInvert/scriptNumberShift do, so the SDK's
|
|
410
|
+
// off-chain state derivation agrees with the deployed script byte-for-byte.
|
|
411
|
+
// runar-sdk does not depend on runar-testing, so this is a local port using
|
|
412
|
+
// this package's own script-number decoder (`decodeScriptNumber` from
|
|
413
|
+
// ./script-utils.js). Note: `encodeScriptNumber` in ./contract.js is NOT
|
|
414
|
+
// reusable here — it returns the full *script push* encoding (with
|
|
415
|
+
// OP_0/OP_1..OP_16/OP_1NEGATE shortcuts), not the raw minimal bytes the
|
|
416
|
+
// bitwise/shift opcodes operate on, so `minimalScriptNumberBytes` below
|
|
417
|
+
// re-derives just the raw-byte core of that algorithm.
|
|
418
|
+
/** Minimal sign-magnitude bytes of a script-number-valued bigint
|
|
419
|
+
* (little-endian, no push-opcode wrapping). */
|
|
420
|
+
/**
|
|
421
|
+
* Mirror the on-chain minimal-encoding rule for a NUMERIC operand.
|
|
422
|
+
*
|
|
423
|
+
* Only values produced by the byte-array ops carry threaded `scriptBytes`, and
|
|
424
|
+
* only those can be non-minimal (`1 >> 1` is `[0x00]`, whose minimal encoding
|
|
425
|
+
* is EMPTY). Everything else is minimal by construction, so this is a no-op.
|
|
426
|
+
*
|
|
427
|
+
* Without it this interpreter re-minimises such a value and reports a spend
|
|
428
|
+
* valid that the deployed covenant aborts on — and since
|
|
429
|
+
* `RunarContract.prepareCall` uses this interpreter to compute the
|
|
430
|
+
* continuation state, it would build a broadcast against a state no node
|
|
431
|
+
* accepts.
|
|
432
|
+
*/
|
|
433
|
+
function assertMinimalNumericOperand(bytes, value, op) {
|
|
434
|
+
if (bytes === undefined || typeof value !== 'bigint')
|
|
435
|
+
return;
|
|
436
|
+
const minimal = minimalScriptNumberBytes(value);
|
|
437
|
+
if (bytes.length === minimal.length && bytes.every((b, i) => b === minimal[i]))
|
|
438
|
+
return;
|
|
439
|
+
throw new Error(`non-minimally encoded script number consumed by '${op}': stack bytes [` +
|
|
440
|
+
bytes.map((b) => b.toString(16).padStart(2, '0')).join(' ') +
|
|
441
|
+
`] decode to ${value}, whose minimal encoding is [` +
|
|
442
|
+
minimal.map((b) => b.toString(16).padStart(2, '0')).join(' ') +
|
|
443
|
+
`]. A node aborts here.`);
|
|
444
|
+
}
|
|
445
|
+
function minimalScriptNumberBytes(n) {
|
|
446
|
+
if (n === 0n)
|
|
447
|
+
return [];
|
|
448
|
+
const negative = n < 0n;
|
|
449
|
+
let abs = negative ? -n : n;
|
|
450
|
+
const bytes = [];
|
|
451
|
+
while (abs > 0n) {
|
|
452
|
+
bytes.push(Number(abs & 0xffn));
|
|
453
|
+
abs >>= 8n;
|
|
454
|
+
}
|
|
455
|
+
const last = bytes[bytes.length - 1];
|
|
456
|
+
if (last & 0x80) {
|
|
457
|
+
bytes.push(negative ? 0x80 : 0x00);
|
|
458
|
+
}
|
|
459
|
+
else if (negative) {
|
|
460
|
+
bytes[bytes.length - 1] = last | 0x80;
|
|
461
|
+
}
|
|
462
|
+
return bytes;
|
|
463
|
+
}
|
|
464
|
+
// The *Bytes helpers operate on RAW stack bytes (the exact byte array a value
|
|
465
|
+
// would occupy on the deployed script's stack), NOT a value's minimal encoding.
|
|
466
|
+
// This matters for CHAINED expressions: a shift/bitwise RESULT can be a
|
|
467
|
+
// non-minimal byte array (e.g. `2 << 8` leaves a 1-byte 0x00), and feeding it to
|
|
468
|
+
// a length-sensitive `& | ^`/shift must see that real length to agree with the
|
|
469
|
+
// deployed script. The interpreter threads these bytes via a per-binding side
|
|
470
|
+
// map (see `evalBindings`); values from other sources are minimal on-chain.
|
|
471
|
+
/**
|
|
472
|
+
* Carry a binding's raw stack bytes across an ALIAS — a binding whose value IS
|
|
473
|
+
* another binding's slot: the `load_const "@ref:<name>"` every local rebind
|
|
474
|
+
* lowers to, an `if` adopting its taken arm's last value, a `loop` adopting its
|
|
475
|
+
* body's. Without this, a chained length-sensitive op re-minimises the aliased
|
|
476
|
+
* value and disagrees with the deployed script (NEW-006: `(4n ^ 4n)` is a
|
|
477
|
+
* 1-byte `0x00` on the stack but `[]` when re-minimised from `0n`).
|
|
478
|
+
*
|
|
479
|
+
* Mirrors `05-stack-lower.ts`, which carries its `rawSlots` marker across the
|
|
480
|
+
* same two constructs.
|
|
481
|
+
*
|
|
482
|
+
* CLEARS when the source has no entry: the alias target is a freshly pushed,
|
|
483
|
+
* minimal value, so a stale entry left by an earlier binding of the SAME name
|
|
484
|
+
* (`let m0 = 4n ^ 4n; m0 = 300n;`) would otherwise be read as this slot's width.
|
|
485
|
+
*/
|
|
486
|
+
function aliasScriptBytes(scriptBytes, from, to) {
|
|
487
|
+
const bytes = scriptBytes[from];
|
|
488
|
+
if (bytes !== undefined) {
|
|
489
|
+
scriptBytes[to] = bytes;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
delete scriptBytes[to];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/** OP_AND/OP_OR/OP_XOR on raw stack bytes. Throws on length mismatch. */
|
|
496
|
+
function scriptNumberBitwiseBytes(op, av, bv) {
|
|
497
|
+
if (av.length !== bv.length) {
|
|
498
|
+
const name = op === '&' ? 'OP_AND' : op === '|' ? 'OP_OR' : 'OP_XOR';
|
|
499
|
+
throw new Error(`${name}: operands must be same length`);
|
|
500
|
+
}
|
|
501
|
+
const result = new Array(av.length);
|
|
502
|
+
for (let i = 0; i < av.length; i++) {
|
|
503
|
+
const x = av[i];
|
|
504
|
+
const y = bv[i];
|
|
505
|
+
result[i] = op === '&' ? x & y : op === '|' ? x | y : x ^ y;
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
/** OP_INVERT: flip every bit of the operand's raw stack bytes (length-preserving). */
|
|
510
|
+
function scriptNumberInvertBytes(av) {
|
|
511
|
+
return av.map((b) => ~b & 0xff);
|
|
512
|
+
}
|
|
513
|
+
/** OP_LSHIFT/OP_RSHIFT on raw stack bytes as a big-endian bit string, preserving
|
|
514
|
+
* byte length. `shift` is the numeric shift count (read as a number on-chain,
|
|
515
|
+
* so only `val`'s bytes are length-significant). Negative shifts fail. */
|
|
516
|
+
function scriptNumberShiftBytes(op, val, shift) {
|
|
517
|
+
if (shift < 0n) {
|
|
518
|
+
throw new Error(op === '<<' ? 'OP_LSHIFT: negative shift' : 'OP_RSHIFT: negative shift');
|
|
519
|
+
}
|
|
520
|
+
const n = Number(shift);
|
|
521
|
+
if (val.length === 0 || n === 0)
|
|
522
|
+
return val.slice();
|
|
523
|
+
let num = 0n;
|
|
524
|
+
for (let i = 0; i < val.length; i++)
|
|
525
|
+
num = (num << 8n) | BigInt(val[i]);
|
|
526
|
+
if (op === '<<') {
|
|
527
|
+
const bitLen = BigInt(val.length * 8);
|
|
528
|
+
num = (num << BigInt(n)) & ((1n << bitLen) - 1n);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
num >>= BigInt(n);
|
|
532
|
+
}
|
|
533
|
+
const result = new Array(val.length);
|
|
534
|
+
for (let i = val.length - 1; i >= 0; i--) {
|
|
535
|
+
result[i] = Number(num & 0xffn);
|
|
536
|
+
num >>= 8n;
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
// bigint -> bigint wrappers (minimal operands) — used by the single-op
|
|
541
|
+
// truth-table tests. The interpreter uses the *Bytes helpers directly so it can
|
|
542
|
+
// thread non-minimal chained intermediates via the side map.
|
|
543
|
+
function scriptNumberBitwise(op, a, b) {
|
|
544
|
+
return decodeScriptNumber(Utils.toHex(scriptNumberBitwiseBytes(op, minimalScriptNumberBytes(a), minimalScriptNumberBytes(b))));
|
|
545
|
+
}
|
|
546
|
+
function scriptNumberInvert(a) {
|
|
547
|
+
return decodeScriptNumber(Utils.toHex(scriptNumberInvertBytes(minimalScriptNumberBytes(a))));
|
|
548
|
+
}
|
|
549
|
+
function scriptNumberShift(op, a, shift) {
|
|
550
|
+
return decodeScriptNumber(Utils.toHex(scriptNumberShiftBytes(op, minimalScriptNumberBytes(a), shift)));
|
|
551
|
+
}
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// Binary operations
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
function evalBinOp(op, left, right, resultType) {
|
|
556
|
+
if (resultType === 'bytes' || (typeof left === 'string' && typeof right === 'string')) {
|
|
557
|
+
return evalBytesBinOp(op, String(left ?? ''), String(right ?? ''));
|
|
558
|
+
}
|
|
559
|
+
const l = toBigInt(left);
|
|
560
|
+
const r = toBigInt(right);
|
|
561
|
+
switch (op) {
|
|
562
|
+
case '+': return l + r;
|
|
563
|
+
case '-': return l - r;
|
|
564
|
+
case '*': return l * r;
|
|
565
|
+
case '/': return r === 0n ? 0n : l / r;
|
|
566
|
+
case '%': return r === 0n ? 0n : l % r;
|
|
567
|
+
case '==':
|
|
568
|
+
case '===': return l === r;
|
|
569
|
+
case '!=':
|
|
570
|
+
case '!==': return l !== r;
|
|
571
|
+
case '<': return l < r;
|
|
572
|
+
case '<=': return l <= r;
|
|
573
|
+
case '>': return l > r;
|
|
574
|
+
case '>=': return l >= r;
|
|
575
|
+
case '&&': return isTruthy(left) && isTruthy(right);
|
|
576
|
+
case '||': return isTruthy(left) || isTruthy(right);
|
|
577
|
+
case '&': return scriptNumberBitwise('&', l, r);
|
|
578
|
+
case '|': return scriptNumberBitwise('|', l, r);
|
|
579
|
+
case '^': return scriptNumberBitwise('^', l, r);
|
|
580
|
+
case '<<': return scriptNumberShift('<<', l, r);
|
|
581
|
+
case '>>': return scriptNumberShift('>>', l, r);
|
|
582
|
+
default: return 0n;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function evalBytesBinOp(op, left, right) {
|
|
586
|
+
switch (op) {
|
|
587
|
+
case '+': // cat
|
|
588
|
+
return left + right;
|
|
589
|
+
case '==':
|
|
590
|
+
case '===':
|
|
591
|
+
return left === right;
|
|
592
|
+
case '!=':
|
|
593
|
+
case '!==':
|
|
594
|
+
return left !== right;
|
|
595
|
+
default:
|
|
596
|
+
return '';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Unary operations
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
function evalUnaryOp(op, operand, resultType) {
|
|
603
|
+
if (resultType === 'bytes') {
|
|
604
|
+
// Bitwise NOT on bytes
|
|
605
|
+
if (op === '~') {
|
|
606
|
+
const hex = String(operand ?? '');
|
|
607
|
+
const bytes = Utils.toArray(hex, 'hex');
|
|
608
|
+
for (let i = 0; i < bytes.length; i++)
|
|
609
|
+
bytes[i] = ~bytes[i] & 0xff;
|
|
610
|
+
return Utils.toHex(bytes);
|
|
611
|
+
}
|
|
612
|
+
return operand;
|
|
613
|
+
}
|
|
614
|
+
const val = toBigInt(operand);
|
|
615
|
+
switch (op) {
|
|
616
|
+
case '-': return -val;
|
|
617
|
+
case '!': return !isTruthy(operand);
|
|
618
|
+
case '~': return scriptNumberInvert(val);
|
|
619
|
+
default: return val;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
// Built-in function calls
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
function evalCall(func, args, realCrypto) {
|
|
626
|
+
switch (func) {
|
|
627
|
+
// Crypto — mocked unless real-crypto context is present.
|
|
628
|
+
case 'checkSig': {
|
|
629
|
+
if (!realCrypto)
|
|
630
|
+
return true;
|
|
631
|
+
return verifyEcdsa(args[0], args[1], realCrypto.sighash);
|
|
632
|
+
}
|
|
633
|
+
case 'checkMultiSig': {
|
|
634
|
+
if (!realCrypto)
|
|
635
|
+
return true;
|
|
636
|
+
return verifyMultiSig(args[0], args[1], realCrypto.sighash);
|
|
637
|
+
}
|
|
638
|
+
case 'checkPreimage': {
|
|
639
|
+
if (!realCrypto)
|
|
640
|
+
return true;
|
|
641
|
+
return verifyPreimage(args[0], realCrypto.sighash);
|
|
642
|
+
}
|
|
643
|
+
// Crypto — real hashes
|
|
644
|
+
case 'sha256': return hashFn('sha256', args[0]);
|
|
645
|
+
case 'hash256': return hashFn('hash256', args[0]);
|
|
646
|
+
case 'hash160': return hashFn('hash160', args[0]);
|
|
647
|
+
case 'ripemd160': return hashFn('ripemd160', args[0]);
|
|
648
|
+
// Assert — skip (on-chain handles it)
|
|
649
|
+
case 'assert': return undefined;
|
|
650
|
+
// Byte operations
|
|
651
|
+
case 'num2bin': {
|
|
652
|
+
const n = toBigInt(args[0]);
|
|
653
|
+
const len = Number(toBigInt(args[1]));
|
|
654
|
+
return num2binHex(n, len);
|
|
655
|
+
}
|
|
656
|
+
case 'bin2num': {
|
|
657
|
+
return bin2numBigInt(String(args[0] ?? ''));
|
|
658
|
+
}
|
|
659
|
+
case 'cat': {
|
|
660
|
+
return String(args[0] ?? '') + String(args[1] ?? '');
|
|
661
|
+
}
|
|
662
|
+
case 'substr': {
|
|
663
|
+
const hex = String(args[0] ?? '');
|
|
664
|
+
const start = Number(toBigInt(args[1]));
|
|
665
|
+
const len = Number(toBigInt(args[2]));
|
|
666
|
+
return hex.slice(start * 2, (start + len) * 2);
|
|
667
|
+
}
|
|
668
|
+
case 'reverseBytes': {
|
|
669
|
+
const hex = String(args[0] ?? '');
|
|
670
|
+
const pairs = [];
|
|
671
|
+
for (let i = 0; i < hex.length; i += 2)
|
|
672
|
+
pairs.push(hex.slice(i, i + 2));
|
|
673
|
+
return pairs.reverse().join('');
|
|
674
|
+
}
|
|
675
|
+
case 'len': {
|
|
676
|
+
const hex = String(args[0] ?? '');
|
|
677
|
+
return BigInt(hex.length / 2);
|
|
678
|
+
}
|
|
679
|
+
// Math builtins
|
|
680
|
+
case 'abs': return toBigInt(args[0]) < 0n ? -toBigInt(args[0]) : toBigInt(args[0]);
|
|
681
|
+
case 'min': return toBigInt(args[0]) < toBigInt(args[1]) ? toBigInt(args[0]) : toBigInt(args[1]);
|
|
682
|
+
case 'max': return toBigInt(args[0]) > toBigInt(args[1]) ? toBigInt(args[0]) : toBigInt(args[1]);
|
|
683
|
+
case 'within': {
|
|
684
|
+
const x = toBigInt(args[0]);
|
|
685
|
+
return x >= toBigInt(args[1]) && x < toBigInt(args[2]);
|
|
686
|
+
}
|
|
687
|
+
case 'safediv': {
|
|
688
|
+
const d = toBigInt(args[1]);
|
|
689
|
+
return d === 0n ? 0n : toBigInt(args[0]) / d;
|
|
690
|
+
}
|
|
691
|
+
case 'safemod': {
|
|
692
|
+
const d = toBigInt(args[1]);
|
|
693
|
+
return d === 0n ? 0n : toBigInt(args[0]) % d;
|
|
694
|
+
}
|
|
695
|
+
case 'clamp': {
|
|
696
|
+
const v = toBigInt(args[0]);
|
|
697
|
+
const lo = toBigInt(args[1]);
|
|
698
|
+
const hi = toBigInt(args[2]);
|
|
699
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
700
|
+
}
|
|
701
|
+
case 'sign': {
|
|
702
|
+
const v = toBigInt(args[0]);
|
|
703
|
+
return v > 0n ? 1n : v < 0n ? -1n : 0n;
|
|
704
|
+
}
|
|
705
|
+
case 'pow': {
|
|
706
|
+
const base = toBigInt(args[0]);
|
|
707
|
+
const exp = toBigInt(args[1]);
|
|
708
|
+
if (exp < 0n)
|
|
709
|
+
return 0n;
|
|
710
|
+
let result = 1n;
|
|
711
|
+
for (let i = 0n; i < exp; i++)
|
|
712
|
+
result *= base;
|
|
713
|
+
return result;
|
|
714
|
+
}
|
|
715
|
+
case 'sqrt': {
|
|
716
|
+
const v = toBigInt(args[0]);
|
|
717
|
+
if (v <= 0n)
|
|
718
|
+
return 0n;
|
|
719
|
+
let x = v;
|
|
720
|
+
let y = (x + 1n) / 2n;
|
|
721
|
+
while (y < x) {
|
|
722
|
+
x = y;
|
|
723
|
+
y = (x + v / x) / 2n;
|
|
724
|
+
}
|
|
725
|
+
return x;
|
|
726
|
+
}
|
|
727
|
+
case 'gcd': {
|
|
728
|
+
let a = toBigInt(args[0]);
|
|
729
|
+
let b = toBigInt(args[1]);
|
|
730
|
+
if (a < 0n)
|
|
731
|
+
a = -a;
|
|
732
|
+
if (b < 0n)
|
|
733
|
+
b = -b;
|
|
734
|
+
while (b !== 0n) {
|
|
735
|
+
const t = b;
|
|
736
|
+
b = a % b;
|
|
737
|
+
a = t;
|
|
738
|
+
}
|
|
739
|
+
return a;
|
|
740
|
+
}
|
|
741
|
+
case 'divmod': {
|
|
742
|
+
const a = toBigInt(args[0]);
|
|
743
|
+
const b = toBigInt(args[1]);
|
|
744
|
+
if (b === 0n)
|
|
745
|
+
return 0n;
|
|
746
|
+
// Returns quotient; in ANF the second result is in a separate binding
|
|
747
|
+
return a / b;
|
|
748
|
+
}
|
|
749
|
+
case 'log2': {
|
|
750
|
+
const v = toBigInt(args[0]);
|
|
751
|
+
if (v <= 0n)
|
|
752
|
+
return 0n;
|
|
753
|
+
let bits = 0n;
|
|
754
|
+
let x = v;
|
|
755
|
+
while (x > 1n) {
|
|
756
|
+
x >>= 1n;
|
|
757
|
+
bits++;
|
|
758
|
+
}
|
|
759
|
+
return bits;
|
|
760
|
+
}
|
|
761
|
+
case 'bool': return isTruthy(args[0]) ? 1n : 0n;
|
|
762
|
+
case 'mulDiv': {
|
|
763
|
+
return (toBigInt(args[0]) * toBigInt(args[1])) / toBigInt(args[2]);
|
|
764
|
+
}
|
|
765
|
+
case 'percentOf': {
|
|
766
|
+
return (toBigInt(args[0]) * toBigInt(args[1])) / 10000n;
|
|
767
|
+
}
|
|
768
|
+
// Baby Bear (p = 2^31 - 2^27 + 1). Needed so StateCovenant.advanceState
|
|
769
|
+
// can auto-derive continuation state instead of failing closed.
|
|
770
|
+
case 'bbFieldMul': {
|
|
771
|
+
const p = 2013265921n;
|
|
772
|
+
return (toBigInt(args[0]) * toBigInt(args[1])) % p;
|
|
773
|
+
}
|
|
774
|
+
case 'merkleRootSha256': {
|
|
775
|
+
let current = String(args[0] ?? '');
|
|
776
|
+
const proof = String(args[1] ?? '');
|
|
777
|
+
const index = toBigInt(args[2]);
|
|
778
|
+
const depth = Number(toBigInt(args[3]));
|
|
779
|
+
for (let i = 0; i < depth; i++) {
|
|
780
|
+
const sibling = proof.slice(i * 64, (i + 1) * 64);
|
|
781
|
+
const bit = (index >> BigInt(i)) & 1n;
|
|
782
|
+
const preimage = bit === 1n ? sibling + current : current + sibling;
|
|
783
|
+
current = hashFn('sha256', preimage);
|
|
784
|
+
}
|
|
785
|
+
return current;
|
|
786
|
+
}
|
|
787
|
+
// Preimage intrinsics — return dummy values in simulation
|
|
788
|
+
case 'extractOutputHash':
|
|
789
|
+
case 'extractAmount':
|
|
790
|
+
return '00'.repeat(32);
|
|
791
|
+
case 'extractLocktime':
|
|
792
|
+
return 0n;
|
|
793
|
+
// Dummy nVersion. Peers (runar-py/rs/zig/rb/java) already return 1;
|
|
794
|
+
// runar-lang runtime does too. Without this arm `prepareCall` fails
|
|
795
|
+
// closed on PreimageExtractors.tick (NEW-006).
|
|
796
|
+
case 'extractVersion':
|
|
797
|
+
return 1n;
|
|
798
|
+
// W7: this arm was MISSING while all six peer SDK interpreters
|
|
799
|
+
// (runar-rs `anf_interpreter.rs`, runar-py, runar-zig — whose comment
|
|
800
|
+
// claims it is mirroring this file — and runar-rb) already returned
|
|
801
|
+
// `0xfffffffe` for it. The omission made every stateful call to a method
|
|
802
|
+
// that reads nSequence fail closed in the TypeScript SDK alone, which is
|
|
803
|
+
// exactly the finality guard #131 tells authors to write. `0xfffffffe` is
|
|
804
|
+
// the SDK's own non-final default (`resolveInputSequence`), so it is the
|
|
805
|
+
// honest off-chain answer for the same reason `extractLocktime` answers 0:
|
|
806
|
+
// the real value is a property of a transaction that does not exist yet.
|
|
807
|
+
case 'extractSequence':
|
|
808
|
+
return 0xfffffffen;
|
|
809
|
+
// Dummy 32-byte prevouts hash. The real hash is a property of a
|
|
810
|
+
// transaction that does not exist yet during off-chain state derivation;
|
|
811
|
+
// lenient mode skips the on-chain assert that binds it. Without this arm
|
|
812
|
+
// `prepareCall` fails closed on every `merge` (NEW-006), so no SDK caller
|
|
813
|
+
// can build the call at all.
|
|
814
|
+
case 'extractHashPrevouts':
|
|
815
|
+
case 'extractHashSequence':
|
|
816
|
+
return '00'.repeat(32);
|
|
817
|
+
case 'extractOutpoint':
|
|
818
|
+
return '00'.repeat(36);
|
|
819
|
+
// Dummy empty scriptCode. The real bytes are a property of a transaction
|
|
820
|
+
// that does not exist yet during off-chain state derivation; lenient mode
|
|
821
|
+
// skips the on-chain asserts that bind them. Without this arm `prepareCall`
|
|
822
|
+
// fails closed on every `merge` that reads extractScriptCode (NEW-006).
|
|
823
|
+
case 'extractScriptCode':
|
|
824
|
+
return '';
|
|
825
|
+
// A-3, KNOWN REMAINING GAP: `extractSigHashType` IS a real runar-lang
|
|
826
|
+
// builtin (`preimage.ts`), unlike its three siblings above it has no
|
|
827
|
+
// dummy value here, and `04-anf-lower.ts` emits it on every stateful
|
|
828
|
+
// method. Measured across this package's suite plus `examples/` and
|
|
829
|
+
// `packages/runar-testing`: 317 evaluations, every one of them consumed
|
|
830
|
+
// by an `assert` that lenient mode skips, so the `undefined` never
|
|
831
|
+
// reaches a state field. Refusing it would break every stateful call for
|
|
832
|
+
// a value nothing reads; giving it a dummy would change strict-mode
|
|
833
|
+
// assert outcomes. Left as-is and pinned by name in
|
|
834
|
+
// `a3-unmodelled-builtin-fails-closed.test.ts` so it is visible rather
|
|
835
|
+
// than silent.
|
|
836
|
+
case 'extractSigHashType':
|
|
837
|
+
return undefined;
|
|
838
|
+
// A-3: compiler-synthesized pseudo-functions, NOT runar-lang builtins —
|
|
839
|
+
// `04-anf-lower.ts` emits these `kind:'call'` nodes itself and none of
|
|
840
|
+
// them is exported by `runar-lang`. They are covenant / constructor
|
|
841
|
+
// scaffolding whose result the SDK reconstructs on its own (the state
|
|
842
|
+
// continuation, the change output, the `super(...)` call), so
|
|
843
|
+
// `undefined` is the correct off-chain answer and must stay
|
|
844
|
+
// non-throwing. `computeStateOutput` runs on every stateful call in the
|
|
845
|
+
// measured corpus; a blanket throw would have broken all of them.
|
|
846
|
+
//
|
|
847
|
+
// `__array_access` (ByteString `data[i]`) is deliberately NOT here: it is
|
|
848
|
+
// a value-producing operation this interpreter genuinely does not model,
|
|
849
|
+
// which is the same defect as an unmodelled builtin, so it falls through
|
|
850
|
+
// to the refusal below.
|
|
851
|
+
//
|
|
852
|
+
// Cross-tier check: every `"func"` appearing in `conformance/
|
|
853
|
+
// anf-interpreter/programs` is either modelled above or one of these four
|
|
854
|
+
// (`buildChangeOutput` and `super` occur there and nowhere else in the
|
|
855
|
+
// measured corpus), so the cross-interpreter parity fixtures still agree
|
|
856
|
+
// with the six peer interpreters after this change.
|
|
857
|
+
case 'computeStateOutput':
|
|
858
|
+
case 'buildChangeOutput':
|
|
859
|
+
case 'super':
|
|
860
|
+
return undefined;
|
|
861
|
+
default:
|
|
862
|
+
// A-3 / NEW-006: FAIL CLOSED. This used to `return undefined`, which
|
|
863
|
+
// made the `try/catch` guard in `contract.ts` structurally unable to
|
|
864
|
+
// fire for the 76 of runar-lang's 105 builtins this interpreter does
|
|
865
|
+
// not model — every EC op, every NIST P-256/P-384 op, all six SLH-DSA
|
|
866
|
+
// verifies, Rabin, `left`/`right`/`split`, `sha256Compress`,
|
|
867
|
+
// `merkleRootSha256`. `contract.ts` then spreads the result over the
|
|
868
|
+
// real state and the `undefined` wins, so a `boolean` state field fed
|
|
869
|
+
// by a signature verifier silently became false and the eventual error
|
|
870
|
+
// pointed at the covenant instead of at this gap. Naming the builtin
|
|
871
|
+
// here is the difference between a five-minute diagnosis and a
|
|
872
|
+
// multi-hour one.
|
|
873
|
+
throw new Error(`ANF interpreter does not model the builtin '${func}'. Its result cannot be ` +
|
|
874
|
+
'computed off-chain, so any state field or data output derived from it would ' +
|
|
875
|
+
'be silently wrong. Pass an explicit `newState` to `call()` if you know the ' +
|
|
876
|
+
"post-state, or add an arm for this builtin to `evalCall` in " +
|
|
877
|
+
'packages/runar-sdk/src/anf-interpreter.ts.');
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function evalMethodCall(callerEnv, methodName, args, stateDelta, dataOutputs, rawOutputs, outputs, anf) {
|
|
881
|
+
// Private method calls appear in the ANF with their bodies available
|
|
882
|
+
// in anf.methods. Execute the method body to compute its return value.
|
|
883
|
+
if (anf) {
|
|
884
|
+
const method = anf.methods.find((m) => m.name === methodName && !m.isPublic);
|
|
885
|
+
if (method) {
|
|
886
|
+
// Build env for the private method: copy property values for load_prop
|
|
887
|
+
const methodEnv = {};
|
|
888
|
+
for (const prop of anf.properties) {
|
|
889
|
+
if (prop.name in callerEnv) {
|
|
890
|
+
methodEnv[prop.name] = callerEnv[prop.name];
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
// Map method params to passed args
|
|
894
|
+
for (let i = 0; i < method.params.length && i < args.length; i++) {
|
|
895
|
+
methodEnv[method.params[i].name] = args[i];
|
|
896
|
+
}
|
|
897
|
+
// Execute the method body — pass real stateDelta so update_prop
|
|
898
|
+
// mutations in private methods are captured
|
|
899
|
+
evalBindings(method.body, methodEnv, stateDelta, dataOutputs, rawOutputs, outputs, anf);
|
|
900
|
+
// Propagate property changes back to the caller's env
|
|
901
|
+
for (const prop of anf.properties) {
|
|
902
|
+
if (prop.name in methodEnv) {
|
|
903
|
+
callerEnv[prop.name] = methodEnv[prop.name];
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// Return the last binding's value (the method's return value)
|
|
907
|
+
if (method.body.length > 0) {
|
|
908
|
+
return methodEnv[method.body[method.body.length - 1].name];
|
|
909
|
+
}
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
// ---------------------------------------------------------------------------
|
|
916
|
+
// Hash helpers
|
|
917
|
+
// ---------------------------------------------------------------------------
|
|
918
|
+
function hashFn(name, input) {
|
|
919
|
+
const hex = String(input ?? '');
|
|
920
|
+
const bytes = Utils.toArray(hex, 'hex');
|
|
921
|
+
let result;
|
|
922
|
+
switch (name) {
|
|
923
|
+
case 'sha256':
|
|
924
|
+
result = Hash.sha256(bytes);
|
|
925
|
+
break;
|
|
926
|
+
case 'hash256':
|
|
927
|
+
result = Hash.hash256(bytes);
|
|
928
|
+
break;
|
|
929
|
+
case 'hash160':
|
|
930
|
+
result = Hash.hash160(bytes);
|
|
931
|
+
break;
|
|
932
|
+
case 'ripemd160':
|
|
933
|
+
result = Hash.ripemd160(bytes);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
return Utils.toHex(result);
|
|
937
|
+
}
|
|
938
|
+
// ---------------------------------------------------------------------------
|
|
939
|
+
// Numeric helpers
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
function toBigInt(v) {
|
|
942
|
+
if (typeof v === 'bigint')
|
|
943
|
+
return v;
|
|
944
|
+
if (typeof v === 'number')
|
|
945
|
+
return BigInt(v);
|
|
946
|
+
if (typeof v === 'boolean')
|
|
947
|
+
return v ? 1n : 0n;
|
|
948
|
+
if (typeof v === 'string') {
|
|
949
|
+
// Handle "42n" format from JSON
|
|
950
|
+
if (/^-?\d+n$/.test(v))
|
|
951
|
+
return BigInt(v.slice(0, -1));
|
|
952
|
+
// Handle plain numeric strings
|
|
953
|
+
if (/^-?\d+$/.test(v))
|
|
954
|
+
return BigInt(v);
|
|
955
|
+
return 0n;
|
|
956
|
+
}
|
|
957
|
+
return 0n;
|
|
958
|
+
}
|
|
959
|
+
function isTruthy(v) {
|
|
960
|
+
if (typeof v === 'boolean')
|
|
961
|
+
return v;
|
|
962
|
+
if (typeof v === 'bigint')
|
|
963
|
+
return v !== 0n;
|
|
964
|
+
if (typeof v === 'number')
|
|
965
|
+
return v !== 0;
|
|
966
|
+
if (typeof v === 'string')
|
|
967
|
+
return v !== '' && v !== '0' && v !== 'false';
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
// ---------------------------------------------------------------------------
|
|
971
|
+
// Byte encoding helpers
|
|
972
|
+
// ---------------------------------------------------------------------------
|
|
973
|
+
/**
|
|
974
|
+
* `num2bin(n, byteLen)` — exactly what OP_NUM2BIN computes (NEW-013).
|
|
975
|
+
*
|
|
976
|
+
* The order of the two steps below is load-bearing. This function used to set
|
|
977
|
+
* the sign bit on the last MAGNITUDE byte and pad zeros AFTER it, so
|
|
978
|
+
* `num2bin(-1n, 2n)` produced `8100` while the script produces `0180`. The
|
|
979
|
+
* result is the bytes the SDK puts in the call transaction, so the wrong order
|
|
980
|
+
* built continuations the deployed script rejects — and six of the seven SDKs
|
|
981
|
+
* shared the mistake, which is why tier-vs-tier parity never caught it.
|
|
982
|
+
*
|
|
983
|
+
* The engine (`@bsv/sdk` `Spend`, and BSV consensus) pads FIRST and then puts
|
|
984
|
+
* the sign bit on the new most-significant byte.
|
|
985
|
+
*/
|
|
986
|
+
function num2binHex(n, byteLen) {
|
|
987
|
+
// 1. Minimal BSV script-number encoding: little-endian magnitude with the
|
|
988
|
+
// sign in bit 7 of the top byte, growing one byte when magnitude data
|
|
989
|
+
// already occupies that bit.
|
|
990
|
+
const negative = n < 0n;
|
|
991
|
+
let abs = negative ? -n : n;
|
|
992
|
+
const bytes = [];
|
|
993
|
+
while (abs > 0n) {
|
|
994
|
+
bytes.push(Number(abs & 0xffn));
|
|
995
|
+
abs >>= 8n;
|
|
996
|
+
}
|
|
997
|
+
if (bytes.length > 0) {
|
|
998
|
+
if ((bytes[bytes.length - 1] & 0x80) !== 0) {
|
|
999
|
+
bytes.push(negative ? 0x80 : 0x00);
|
|
1000
|
+
}
|
|
1001
|
+
else if (negative) {
|
|
1002
|
+
bytes[bytes.length - 1] |= 0x80;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// 2a. Field too narrow for the value: OP_NUM2BIN rejects this outright
|
|
1006
|
+
// ("impossible encoding"). The interpreter keeps its historical
|
|
1007
|
+
// truncation rather than growing a new failure mode here; an equal-length
|
|
1008
|
+
// encoding is already final and needs no sign-bit move.
|
|
1009
|
+
if (bytes.length >= byteLen) {
|
|
1010
|
+
return bytes
|
|
1011
|
+
.slice(0, byteLen)
|
|
1012
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1013
|
+
.join('');
|
|
1014
|
+
}
|
|
1015
|
+
// 2b. Padded: lift the sign bit off the magnitude, zero-extend, and re-apply
|
|
1016
|
+
// it to the byte that is now most significant.
|
|
1017
|
+
let signBit = 0;
|
|
1018
|
+
if (bytes.length > 0) {
|
|
1019
|
+
signBit = bytes[bytes.length - 1] & 0x80;
|
|
1020
|
+
bytes[bytes.length - 1] &= 0x7f;
|
|
1021
|
+
}
|
|
1022
|
+
while (bytes.length < byteLen)
|
|
1023
|
+
bytes.push(0x00);
|
|
1024
|
+
if (signBit !== 0)
|
|
1025
|
+
bytes[byteLen - 1] |= 0x80;
|
|
1026
|
+
return bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
1027
|
+
}
|
|
1028
|
+
// ---------------------------------------------------------------------------
|
|
1029
|
+
// Real ECDSA / preimage verification (used by executeOnChainAuthoritative)
|
|
1030
|
+
// ---------------------------------------------------------------------------
|
|
1031
|
+
function toByteArray(v) {
|
|
1032
|
+
if (typeof v === 'string') {
|
|
1033
|
+
if (v.length % 2 !== 0)
|
|
1034
|
+
return null;
|
|
1035
|
+
if (!/^[0-9a-fA-F]*$/.test(v))
|
|
1036
|
+
return null;
|
|
1037
|
+
return Utils.toArray(v, 'hex');
|
|
1038
|
+
}
|
|
1039
|
+
if (v instanceof Uint8Array)
|
|
1040
|
+
return Array.from(v);
|
|
1041
|
+
if (Array.isArray(v))
|
|
1042
|
+
return v;
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
function parseDerSignatureMaybeWithSighashByte(bytes) {
|
|
1046
|
+
if (bytes.length < 8 || bytes[0] !== 0x30)
|
|
1047
|
+
return null;
|
|
1048
|
+
const declared = bytes[1];
|
|
1049
|
+
const expected = declared + 2;
|
|
1050
|
+
let pure;
|
|
1051
|
+
if (bytes.length === expected) {
|
|
1052
|
+
pure = bytes;
|
|
1053
|
+
}
|
|
1054
|
+
else if (bytes.length === expected + 1) {
|
|
1055
|
+
// Drop trailing sighash type byte (e.g. 0x41 SIGHASH_ALL|FORKID).
|
|
1056
|
+
pure = bytes.slice(0, expected);
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
pure = bytes;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
return Signature.fromDER(pure);
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function verifyEcdsa(sigVal, pkVal, sighash) {
|
|
1069
|
+
const sigBytes = toByteArray(sigVal);
|
|
1070
|
+
const pkBytes = toByteArray(pkVal);
|
|
1071
|
+
if (!sigBytes || !pkBytes)
|
|
1072
|
+
return false;
|
|
1073
|
+
try {
|
|
1074
|
+
const pubKey = PublicKey.fromDER(pkBytes);
|
|
1075
|
+
const sig = parseDerSignatureMaybeWithSighashByte(sigBytes);
|
|
1076
|
+
if (!sig)
|
|
1077
|
+
return false;
|
|
1078
|
+
// Raw ECDSA verify: treat `sighash` as the message digest directly,
|
|
1079
|
+
// matching the on-chain CHECKSIG semantic (and the cross-tier real-
|
|
1080
|
+
// crypto fixture convention). `pubKey.verify(msg, sig)` would re-hash
|
|
1081
|
+
// `msg` with sha256 internally, which makes the TS verification
|
|
1082
|
+
// disagree with every other SDK that simply ECDSA-verifies the
|
|
1083
|
+
// supplied 32-byte digest.
|
|
1084
|
+
const msgBN = new BigNumber(sighash);
|
|
1085
|
+
return ecdsaVerifyRaw(msgBN, sig, pubKey);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function verifyMultiSig(sigsVal, pksVal, sighash) {
|
|
1092
|
+
if (!Array.isArray(sigsVal) || !Array.isArray(pksVal))
|
|
1093
|
+
return false;
|
|
1094
|
+
if (sigsVal.length > pksVal.length)
|
|
1095
|
+
return false;
|
|
1096
|
+
let pkIdx = 0;
|
|
1097
|
+
for (const sig of sigsVal) {
|
|
1098
|
+
let matched = false;
|
|
1099
|
+
while (pkIdx < pksVal.length) {
|
|
1100
|
+
const ok = verifyEcdsa(sig, pksVal[pkIdx], sighash);
|
|
1101
|
+
pkIdx++;
|
|
1102
|
+
if (ok) {
|
|
1103
|
+
matched = true;
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (!matched)
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* BIP-143 / OP_PUSH_TX semantic: the on-chain check is
|
|
1114
|
+
* `hash256(preimage) === sighash`. We replicate that: the supplied preimage
|
|
1115
|
+
* is the serialised BIP-143 message; its double-SHA-256 must equal the
|
|
1116
|
+
* sighash the caller provided to {@link executeOnChainAuthoritative}.
|
|
1117
|
+
*/
|
|
1118
|
+
function verifyPreimage(preimageVal, sighash) {
|
|
1119
|
+
const preBytes = toByteArray(preimageVal);
|
|
1120
|
+
if (!preBytes)
|
|
1121
|
+
return false;
|
|
1122
|
+
const computed = Hash.hash256(preBytes);
|
|
1123
|
+
if (computed.length !== sighash.length)
|
|
1124
|
+
return false;
|
|
1125
|
+
for (let i = 0; i < sighash.length; i++) {
|
|
1126
|
+
if (computed[i] !== sighash[i])
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
function bin2numBigInt(hex) {
|
|
1132
|
+
if (!hex || hex.length === 0)
|
|
1133
|
+
return 0n;
|
|
1134
|
+
const bytes = [];
|
|
1135
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
1136
|
+
bytes.push(parseInt(hex.slice(i, i + 2), 16));
|
|
1137
|
+
}
|
|
1138
|
+
if (bytes.length === 0)
|
|
1139
|
+
return 0n;
|
|
1140
|
+
const negative = (bytes[bytes.length - 1] & 0x80) !== 0;
|
|
1141
|
+
if (negative) {
|
|
1142
|
+
bytes[bytes.length - 1] &= 0x7f;
|
|
1143
|
+
}
|
|
1144
|
+
let result = 0n;
|
|
1145
|
+
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
1146
|
+
result = (result << 8n) | BigInt(bytes[i]);
|
|
1147
|
+
}
|
|
1148
|
+
return negative ? -result : result;
|
|
1149
|
+
}
|