@easy1staking/cip113-sdk-ts 0.7.0 → 0.10.0

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.
Files changed (35) hide show
  1. package/README.md +118 -2
  2. package/blueprints/standard/v0.5.0-alpha.3/UPSTREAM_PIN.json +35 -0
  3. package/blueprints/standard/v0.5.0-alpha.3/plutus.json +1126 -0
  4. package/blueprints/standard/v0.5.0-alpha.4/UPSTREAM_PIN.json +36 -0
  5. package/blueprints/standard/v0.5.0-alpha.4/plutus.json +1434 -0
  6. package/dist/core/evo-utils.d.ts +357 -39
  7. package/dist/core/evo-utils.d.ts.map +1 -1
  8. package/dist/core/evo-utils.js +513 -27
  9. package/dist/core/evo-utils.js.map +1 -1
  10. package/dist/core/ledger-order.d.ts +204 -18
  11. package/dist/core/ledger-order.d.ts.map +1 -1
  12. package/dist/core/ledger-order.js +385 -28
  13. package/dist/core/ledger-order.js.map +1 -1
  14. package/dist/index.d.ts +6 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +15 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/standard/blueprint.d.ts +129 -29
  19. package/dist/standard/blueprint.d.ts.map +1 -1
  20. package/dist/standard/blueprint.js +245 -47
  21. package/dist/standard/blueprint.js.map +1 -1
  22. package/dist/standard/scripts.d.ts +189 -75
  23. package/dist/standard/scripts.d.ts.map +1 -1
  24. package/dist/standard/scripts.js +454 -123
  25. package/dist/standard/scripts.js.map +1 -1
  26. package/dist/substandards/dummy/index.d.ts +19 -0
  27. package/dist/substandards/dummy/index.d.ts.map +1 -1
  28. package/dist/substandards/dummy/index.js +322 -73
  29. package/dist/substandards/dummy/index.js.map +1 -1
  30. package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
  31. package/dist/substandards/freeze-and-seize/index.js +455 -117
  32. package/dist/substandards/freeze-and-seize/index.js.map +1 -1
  33. package/dist/types.d.ts +161 -48
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +2 -1
@@ -1,34 +1,48 @@
1
1
  /**
2
- * Standard script parameterization — CIP-113 0.5.0-alpha.2 (upstream 9db7e06).
2
+ * Standard script parameterization — CIP-113 0.5.0-alpha.4
3
+ * (upstream 7e8a63198c5b240135f1aa2f043ce5d7c046b2c4).
3
4
  *
4
5
  * Uses Evolution SDK directly for UPLC.applyParamsToScript and ScriptHash.
5
6
  *
6
- * Dependency graph. Note it is NO LONGER a single chain: upstream #110 removed
7
- * programmable_logic_global, and PLB is now parameterised by the params-NFT
8
- * policy rather than by PLG's credential, so everything downstream hangs off
9
- * `params_policy` in parallel instead of in series.
7
+ * Dependency graph. It is NOT a single chain: `programmable_logic_base` hangs
8
+ * off the params-NFT policy, so everything downstream of it fans out in
9
+ * parallel rather than in series.
10
10
  *
11
- * always_fail(nonce) -> hash (issuance side only now)
12
- * coordination_spend(nonce) -> hash NEW: the lock target
13
- * protocol_params_mint(utxo_ref, coord_hash) -> policy == params_policy
11
+ * always_fail(nonce) -> hash (issuance side only)
12
+ * protocol_params(utxo_ref) -> policy == params_policy
14
13
  * |
15
- * +-- programmable_logic_base(params_policy) -> hash
16
- * +-- transfer(params_policy) -> hash (PLG's transfer arm, renamed)
17
- * +-- third_party(params_policy) -> hash NEW: seize / clawback
18
- * +-- unfracking(params_policy) -> hash
19
- * +-- registry_spend(params_policy) -> hash
14
+ * +-- programmable_logic_base(params_policy) -> hash == plb
15
+ * | |
16
+ * | +-- transfer(Script(plb), registry_node_cs, max_inline) -> hash
17
+ * | +-- third_party(Script(plb), registry_node_cs, max_inline) -> hash
18
+ * | +-- unfracking(Script(plb), registry_node_cs, max_inline) -> hash
19
+ * | +-- issuance_logic(Script(plb), registry_node_cs,
20
+ * | params_policy, max_inline) -> hash NEW in alpha.4
21
+ * |
22
+ * +-- issuance_mint(Script(minting_logic), params_policy) -> policy
23
+ *
24
+ * programmable_logic_global(transfer_hash, third_party_hash, unfracking_hash)
25
+ * -> hash (built LAST)
20
26
  *
21
- * issuance_cbor_hex_mint(utxo_ref, always_fail_hash) -> policy
22
- * registry_mint(utxo_ref, issuance_cbor_hex_cs, registry_spend_cred) -> policy
23
- * issuance_mint(PLB_cred, registry_node_cs, minting_logic_cred, params_policy)
24
- * upgrade_multisig(signers, threshold) -> hash (independent)
27
+ * issuance_cbor_hex_mint(utxo_ref, always_fail_hash) -> policy
28
+ * registry(utxo_ref, issuance_cbor_hex_cs) -> policy
29
+ *
30
+ * upgrade_multisig(utxo_ref) -> hash (independent one-shot)
25
31
  *
26
32
  * ⚠ PARAMETER TYPES ARE NOT INTERCHANGEABLE and TypeScript cannot tell them
27
33
  * apart — every one of these is a hex string at the call site. `params_policy`
28
- * is a PolicyId (a bare ByteArray); `minting_logic_cred` and friends are
29
- * Credentials (a constructor-wrapped Script/VerificationKey). Passing a policy
30
- * where a credential belongs produces a valid script with the wrong hash. The
31
- * types below come from the blueprint's own parameter schemas, not from
34
+ * and `registry_node_cs` are PolicyIds (bare ByteArrays); `minting_logic_cred`
35
+ * and `programmable_logic_base` are Credentials (constructor-wrapped
36
+ * Script/VerificationKey). `scriptCredential()` wraps; `Data.bytearray()` does
37
+ * not. Passing a policy where a credential belongs produces a valid script with
38
+ * the wrong hash.
39
+ *
40
+ * ⛔ alpha.4 makes that sharper: `issuance_logic` takes TWO ADJACENT PolicyId
41
+ * parameters — `registry_node_cs` then `params_policy`. They are the same type,
42
+ * the same length, and both are `string` here. Swapping them yields a script
43
+ * that builds, hashes and deploys, and nothing before the ledger will say so.
44
+ *
45
+ * The types below come from the blueprint's own parameter schemas, not from
32
46
  * upstream's prose docs — see the hazard note in blueprint.ts.
33
47
  */
34
48
  import { Data } from "@evolution-sdk/evolution";
@@ -55,14 +69,8 @@ export function createStandardScripts(blueprint, onParameterize) {
55
69
  alwaysFail(nonce) {
56
70
  return parameterize(STANDARD_VALIDATORS.ALWAYS_FAIL, [Data.bytearray(nonce)]);
57
71
  },
58
- coordinationSpend(nonce) {
59
- return parameterize(STANDARD_VALIDATORS.COORDINATION_SPEND, [Data.bytearray(nonce)]);
60
- },
61
- protocolParamsMint(utxoRef, coordinationHash) {
62
- return parameterize(STANDARD_VALIDATORS.PROTOCOL_PARAMS_MINT, [
63
- outputReference(utxoRef),
64
- Data.bytearray(coordinationHash),
65
- ]);
72
+ protocolParams(utxoRef) {
73
+ return parameterize(STANDARD_VALIDATORS.PROTOCOL_PARAMS, [outputReference(utxoRef)]);
66
74
  },
67
75
  programmableLogicBase(paramsPolicy) {
68
76
  // PolicyId — a bare ByteArray. It was scriptCredential(plgHash) before #110.
@@ -70,150 +78,464 @@ export function createStandardScripts(blueprint, onParameterize) {
70
78
  Data.bytearray(paramsPolicy),
71
79
  ]);
72
80
  },
73
- transfer(paramsPolicy) {
74
- return parameterize(STANDARD_VALIDATORS.TRANSFER, [Data.bytearray(paramsPolicy)]);
75
- },
76
- thirdParty(paramsPolicy) {
77
- return parameterize(STANDARD_VALIDATORS.THIRD_PARTY, [Data.bytearray(paramsPolicy)]);
81
+ transfer(progLogicCred, registryPolicy, maxInlineDatumBytes) {
82
+ return parameterize(STANDARD_VALIDATORS.TRANSFER, [
83
+ scriptCredential(progLogicCred),
84
+ Data.bytearray(registryPolicy),
85
+ Data.int(BigInt(maxInlineDatumBytes)),
86
+ ]);
78
87
  },
79
- unfracking(paramsPolicy) {
80
- return parameterize(STANDARD_VALIDATORS.UNFRACKING, [Data.bytearray(paramsPolicy)]);
88
+ thirdParty(progLogicCred, registryPolicy, maxInlineDatumBytes) {
89
+ return parameterize(STANDARD_VALIDATORS.THIRD_PARTY, [
90
+ scriptCredential(progLogicCred),
91
+ Data.bytearray(registryPolicy),
92
+ Data.int(BigInt(maxInlineDatumBytes)),
93
+ ]);
81
94
  },
82
- upgradeMultisig(signers, threshold) {
83
- return parameterize(STANDARD_VALIDATORS.UPGRADE_MULTISIG, [
84
- Data.list(signers.map((s) => Data.bytearray(s))),
85
- Data.int(BigInt(threshold)),
95
+ unfracking(progLogicCred, registryPolicy, maxInlineDatumBytes) {
96
+ return parameterize(STANDARD_VALIDATORS.UNFRACKING, [
97
+ scriptCredential(progLogicCred),
98
+ Data.bytearray(registryPolicy),
99
+ Data.int(BigInt(maxInlineDatumBytes)),
86
100
  ]);
87
101
  },
102
+ upgradeMultisig(utxoRef) {
103
+ return parameterize(STANDARD_VALIDATORS.UPGRADE_MULTISIG, [outputReference(utxoRef)]);
104
+ },
88
105
  issuanceCborHexMint(utxoRef, alwaysFailHash) {
89
106
  return parameterize(STANDARD_VALIDATORS.ISSUANCE_CBOR_HEX_MINT, [
90
107
  outputReference(utxoRef),
91
108
  Data.bytearray(alwaysFailHash),
92
109
  ]);
93
110
  },
94
- registryMint(utxoRef, issuanceCborHexPolicy, registrySpendHash) {
95
- return parameterize(STANDARD_VALIDATORS.REGISTRY_MINT, [
111
+ registry(utxoRef, issuanceCborHexPolicy) {
112
+ return parameterize(STANDARD_VALIDATORS.REGISTRY, [
96
113
  outputReference(utxoRef),
97
114
  Data.bytearray(issuanceCborHexPolicy),
98
- scriptCredential(registrySpendHash),
99
115
  ]);
100
116
  },
101
- registrySpend(paramsPolicy) {
102
- return parameterize(STANDARD_VALIDATORS.REGISTRY_SPEND, [
103
- Data.bytearray(paramsPolicy),
117
+ programmableLogicGlobal(transferHash, thirdPartyHash, unfrackingHash) {
118
+ // Bare ScriptHashes — NOT scriptCredential(). The blueprint declares
119
+ // `aiken/crypto/ScriptHash` here, while issuance_mint declares
120
+ // `cardano/address/Credential` for the same 28 bytes. Wrapping these
121
+ // would produce a different script that still hashes and still deploys.
122
+ return parameterize(STANDARD_VALIDATORS.PROGRAMMABLE_LOGIC_GLOBAL, [
123
+ Data.bytearray(transferHash),
124
+ Data.bytearray(thirdPartyHash),
125
+ Data.bytearray(unfrackingHash),
104
126
  ]);
105
127
  },
106
- issuanceMint(plbHash, registryNodePolicy, mintingLogicHash, paramsPolicy) {
128
+ issuanceMint(mintingLogicHash, paramsPolicy) {
107
129
  return parameterize(STANDARD_VALIDATORS.ISSUANCE_MINT, [
108
- scriptCredential(plbHash),
109
- Data.bytearray(registryNodePolicy),
110
130
  scriptCredential(mintingLogicHash),
111
131
  Data.bytearray(paramsPolicy),
112
132
  ]);
113
133
  },
134
+ issuanceLogic(progLogicCred, registryPolicy, paramsPolicy, maxInlineDatumBytes) {
135
+ // Credential, then TWO bare PolicyIds in blueprint order
136
+ // (registry_node_cs, params_policy), then the Int. See the interface
137
+ // doc: the two policies are the swap hazard alpha.4 introduced.
138
+ return parameterize(STANDARD_VALIDATORS.ISSUANCE_LOGIC, [
139
+ scriptCredential(progLogicCred),
140
+ Data.bytearray(registryPolicy),
141
+ Data.bytearray(paramsPolicy),
142
+ Data.int(BigInt(maxInlineDatumBytes)),
143
+ ]);
144
+ },
114
145
  };
115
146
  }
116
- /** Thrown when a blueprint does not reproduce its deployment's script hashes. */
147
+ /**
148
+ * Thrown when a deployment record and the blueprint do not describe the same
149
+ * protocol instance.
150
+ *
151
+ * ⛔ IT IS THE CLASS CONSUMERS CATCH, so every refusal from this module must
152
+ * be one. The documented consumer shape is
153
+ * `catch (e) { if (e instanceof DeploymentMismatchError) … }`; a bare `Error`
154
+ * thrown beside it drops silently into whatever generic handler comes next,
155
+ * which is how a refusal that names a field ends up reported as "something went
156
+ * wrong".
157
+ *
158
+ * ⚠ `mismatches` IS EMPTY FOR REFUSALS THAT ARE NOT HASH COMPARISONS, and the
159
+ * emptiness is the signal rather than an oversight: nothing derived disagreed
160
+ * with anything recorded — the record was malformed, or held a value that was
161
+ * never a legal choice. Those pass their own `detail` message, because the
162
+ * composed "N script hash(es) differ" text would describe a comparison that
163
+ * never happened.
164
+ */
117
165
  export class DeploymentMismatchError extends Error {
118
166
  mismatches;
119
- constructor(mismatches, blueprintTitle) {
120
- super(`Blueprint "${blueprintTitle}" does not reproduce this deployment. ` +
121
- `${mismatches.length} script hash(es) differ:\n` +
122
- mismatches
123
- .map((m) => ` ${m.name}: derived ${m.derived}, deployment says ${m.deployed}`)
124
- .join("\n") +
125
- `\nThe blueprint and the DeploymentParams describe different protocol instances. ` +
126
- `Transactions built from this pairing would be rejected at submission.`);
167
+ constructor(mismatches, blueprintTitle, detail) {
168
+ super(detail ??
169
+ `Blueprint "${blueprintTitle}" does not reproduce this deployment. ` +
170
+ `${mismatches.length} script hash(es) differ:\n` +
171
+ mismatches
172
+ .map((m) => ` ${m.name}: derived ${m.derived}, deployment says ${m.deployed}`)
173
+ .join("\n") +
174
+ `\nThe blueprint and the DeploymentParams describe different protocol instances. ` +
175
+ `Transactions built from this pairing would be rejected at submission.` +
176
+ dispatcherCauseHint(mismatches));
127
177
  this.name = "DeploymentMismatchError";
128
178
  this.mismatches = mismatches;
129
179
  }
130
180
  }
181
+ /** The name `deriveDeploymentScripts` gives the dispatcher-coherence check. */
182
+ const PLG_CHECK_NAME = "programmable_logic_global (dispatcher coherence)";
131
183
  /**
132
- * Derive every parameterizable standard script hash from the blueprint and
133
- * check it against DeploymentParams. Throws DeploymentMismatchError on any
134
- * difference; returns the full check list on success.
135
- *
136
- * Why this exists: parameterization changes are not always visible to the
137
- * compiler. Upstream has changed a parameter's *meaning* while keeping its
138
- * arity and type (protocol_params_mint's `always_fail_hash` became
139
- * `coordination_addr_hash` in 0.5.0-alpha.1), so a wrong value typechecks,
140
- * builds, and only fails when the ledger rejects the transaction. This is the
141
- * check that catches it, and it is why buildDeploymentScripts no longer
142
- * overwrites derived hashes with deployment values.
143
- *
144
- * Not covered: always_fail (its nonce is not carried in DeploymentParams),
145
- * issuance_mint (parameterized per minting logic), and upgrade_multisig (its
146
- * signers/threshold are an authority choice, not a derived protocol value).
147
- * coordination_spend IS covered — DeploymentParams carries its nonce precisely
148
- * so the lock target can be re-derived rather than trusted.
149
- *
150
- * WHERE THIS CHECK HAS VALUE — and where it has none.
151
- *
152
- * It is only meaningful when the blueprint and the deployment come from
153
- * INDEPENDENT sources, so that they can actually disagree: a deployment loaded
154
- * from disk, a database, or the chain, checked against the blueprint currently
155
- * bundled. That is the case it catches, and the failure it catches is real —
156
- * this repo shipped a blueprint swapped in place under an unchanged directory
157
- * name, with 4 of 8 validator hashes moved.
158
- *
159
- * It proves NOTHING at bootstrap time. A deployment script derives the hashes,
160
- * populates DeploymentParams from those same values, and then asserts against
161
- * them — a tautology that cannot fail. Do not read a passing assertion inside a
162
- * bootstrap as evidence that the deployment is correct; assert on LOAD instead.
184
+ * THE DISPATCHER CHECK IS REACHABLE FROM THREE CAUSES AND ITS NAME ONLY
185
+ * SUGGESTS ONE. "dispatcher coherence" trains the reader to look for a stale
186
+ * delegate, so a mismatch caused by the RECORDED `unfrackingParameter` sends
187
+ * them to the wrong two fields — and the parameter is the one cause the reader
188
+ * cannot see by comparing the record against itself. The check's name is pinned
189
+ * by `test/deployment-assertion.test.mjs`; the causes are named here instead.
163
190
  */
164
- export function assertDeploymentScripts(blueprint, deployment) {
191
+ function dispatcherCauseHint(mismatches) {
192
+ if (!mismatches.some((m) => m.name === PLG_CHECK_NAME))
193
+ return "";
194
+ return (`\n\n${PLG_CHECK_NAME} is derived from THREE recorded values, and any one of ` +
195
+ `them can cause it:\n` +
196
+ ` transfer.scriptHash\n` +
197
+ ` thirdParty.scriptHash\n` +
198
+ ` programmableLogicGlobal.unfrackingParameter — what the dispatcher was ` +
199
+ `COMPILED AGAINST, which is NOT necessarily unfracking.scriptHash: a deployment ` +
200
+ `may record UNFRACKING_DISABLED here while deploying the real unfracking script ` +
201
+ `beside it. Check this one before the delegates; it is the only one of the three ` +
202
+ `that cannot be checked against anything else in the record.`);
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // The unfracking parameter — a deployment CHOICE baked into the dispatcher
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * The `unfracking_hash` a dispatcher is compiled against when the deployment
209
+ * wants unfracking DEPLOYED BUT UNREACHABLE: 28 zero bytes.
210
+ *
211
+ * ⚠ IT IS BAKED INTO THE DISPATCHER'S HASH, so two deployments that chose
212
+ * differently are DIFFERENT PROTOCOLS. Same category as `maxInlineDatumBytes`
213
+ * (see `types.ts`): a deployment CHOICE, recoverable from no hash, which is why
214
+ * `DeploymentParams.programmableLogicGlobal.unfrackingParameter` records the
215
+ * value itself rather than a flag.
216
+ *
217
+ * ⚠ WHY 28 BYTES AND NOT `#""`. The Aiken parameter is a bare `ScriptHash`
218
+ * — a `ByteArray` with no length constraint, not an `Option`, so there is no
219
+ * "none" to reach for. `#""` is type-legal, but NOBODY HERE CAN READ THE
220
+ * COMPILED VALIDATOR'S BODY: whether it asserts a length, or builds an address
221
+ * out of this parameter, is unknown. A 28-byte value is structurally identical
222
+ * to a real script hash, so every path behaves normally.
223
+ * ⇒ Choose a value that cannot reach code we cannot read, rather than reason
224
+ * about what that code probably does.
225
+ *
226
+ * ⚠ NOTHING HASHES TO IT. At 2^224 that is an impossibility, not a low risk,
227
+ * so the sentinel can never collide with a real `unfracking` deployment. Zeros
228
+ * rather than `FF…FF`, which reads as a mask or a placeholder.
229
+ *
230
+ * ⛔ IT LIVES HERE AND NOWHERE ELSE. The platform must IMPORT it. A
231
+ * platform-side copy is a second place to disagree about a value that
232
+ * determines a script hash.
233
+ */
234
+ export const UNFRACKING_DISABLED = "00".repeat(28);
235
+ /**
236
+ * Refuse a recorded `unfrackingParameter` that is neither of the two values a
237
+ * deployment may legitimately hold.
238
+ *
239
+ * ⛔ THIS IS A TWO-ELEMENT SET MEMBERSHIP, NOT A "DOESN'T MATCH ⇒ SENTINEL"
240
+ * FALLBACK. `if (recorded !== derived) { assume sentinel }` is the shape that
241
+ * passes everything: it accepts a typo, a truncation, and a stale hash from a
242
+ * different deployment — each of which then silently becomes the value the
243
+ * dispatcher is built from. Both candidates are named explicitly, and so is the
244
+ * value that was actually found; the standing discipline is
245
+ * `test/provenance-artefact.test.mjs` — refused by a NAMED assertion, never by
246
+ * a `??`/`||` guess.
247
+ *
248
+ * ⚠ THE CASE ONLY THIS CATCHES, and the reason it has to exist at all: a
249
+ * bootstrap that compiled the dispatcher against a wrong value and then recorded
250
+ * BOTH. Every one of the ten hash checks then reproduces — the record is
251
+ * SELF-CONSISTENT, so there is nothing for a comparison to disagree with — and
252
+ * only this membership test says the parameter was never a legal choice.
253
+ */
254
+ const CANONICAL_SCRIPT_HASH = /^[0-9a-f]{56}$/;
255
+ /**
256
+ * Read `programmableLogicGlobal.unfrackingParameter` and refuse anything that
257
+ * is not a WELL-FORMED script hash — BEFORE a single script is derived.
258
+ *
259
+ * ⛔ REFUSAL 1, THE OWN-PROPERTY CHECK, AND IT IS NOT DEFENSIVE PROGRAMMING.
260
+ * With `Object.prototype.unfrackingParameter` set, a record with the field
261
+ * GENUINELY ABSENT reads back a value, passes every one of the ten hash checks,
262
+ * and builds the dispatcher from something nobody recorded. MEASURED, not
263
+ * feared. This repo already treats that exact shape as a defect class —
264
+ * `test/ledger-order.test.mjs`, *"refuses an inherited `Object.prototype` key"*
265
+ * — so the standard exists and this call site has to meet it. `in` and a
266
+ * truthiness test both walk the prototype chain; only `hasOwnProperty` does not.
267
+ *
268
+ * ⛔ REFUSAL 2, THE SHAPE CHECK. A stray space, a newline from a copy-paste, a
269
+ * `0x` prefix off a block explorer, a `Uint8Array`, a `Buffer`, `null` — every
270
+ * one of these used to fall through to Evolution's
271
+ * `ParseError: Data.ByteArray … Expected string`, which names NO field, NO
272
+ * record and NO file, in front of an artefact that cannot be regenerated.
273
+ * Fails closed either way; the whole difference is whether the deployer can act
274
+ * on it.
275
+ *
276
+ * ⚠ BYTE-EXACT, AND DELIBERATELY NOT NORMALISING. An uppercase spelling of a
277
+ * legal hash is REFUSED and told it is a CASE problem — never lowercased and
278
+ * accepted. This value is what the dispatcher was COMPILED AGAINST; accepting a
279
+ * record whose spelling differs from what was hashed would re-open the exact
280
+ * hole this ticket exists to close.
281
+ *
282
+ * ⚠ TWO REFUSALS, TWO MESSAGES, AND THEY ANSWER DIFFERENT QUESTIONS — *is this
283
+ * a well-formed script hash?* and *is it one of the two this deployment may
284
+ * record?* (`refuseOnUnfrackingParameter`, below). A reader who cannot tell
285
+ * which one fired cannot act, so they are never merged into one condition.
286
+ */
287
+ function readUnfrackingParameter(deployment, blueprintTitle) {
288
+ const plg = deployment.programmableLogicGlobal;
289
+ if (!Object.prototype.hasOwnProperty.call(plg, "unfrackingParameter")) {
290
+ throw new DeploymentMismatchError([], blueprintTitle, `programmableLogicGlobal.unfrackingParameter is ABSENT from this deployment ` +
291
+ `record — it has no OWN property of that name. It is REQUIRED and it has no ` +
292
+ `default: it records what programmable_logic_global was COMPILED AGAINST, ` +
293
+ `either the real unfracking script hash or UNFRACKING_DISABLED ` +
294
+ `(${UNFRACKING_DISABLED}), and NOTHING IN THE RECORD DETERMINES WHICH. ` +
295
+ `unfracking.scriptHash is not it: a deployment may deploy, publish and ` +
296
+ `record the real unfracking script while compiling the dispatcher against ` +
297
+ `the sentinel, so defaulting to it would be right often enough that nobody ` +
298
+ `would ever check. Add the field to the record. ` +
299
+ `⚠ An inherited Object.prototype.unfrackingParameter does NOT satisfy this: ` +
300
+ `it would hand the dispatcher a value no deployment recorded.`);
301
+ }
302
+ const recorded = plg.unfrackingParameter;
303
+ if (typeof recorded !== "string" || !CANONICAL_SCRIPT_HASH.test(recorded)) {
304
+ const shown = typeof recorded === "string" ? JSON.stringify(recorded) : describe(recorded);
305
+ const caseOnly = typeof recorded === "string" &&
306
+ CANONICAL_SCRIPT_HASH.test(recorded.toLowerCase()) &&
307
+ recorded !== recorded.toLowerCase();
308
+ throw new DeploymentMismatchError([], blueprintTitle, `programmableLogicGlobal.unfrackingParameter records ${shown}, which is not a ` +
309
+ `well-formed script hash. It must be EXACTLY 56 LOWERCASE HEX CHARACTERS ` +
310
+ `(28 bytes), with no 0x prefix, no whitespace and no surrounding quotes.` +
311
+ (caseOnly
312
+ ? ` ⚠ THIS IS A CASE PROBLEM, AND ONLY A CASE PROBLEM: lowercased, this IS ` +
313
+ `56 hex characters. It is refused rather than normalised because this value ` +
314
+ `is what the dispatcher was COMPILED AGAINST — accepting a spelling that ` +
315
+ `differs from the bytes that were hashed is the ambiguity this field exists ` +
316
+ `to remove. Write it in lowercase in the record.`
317
+ : ``) +
318
+ `\nThis check runs BEFORE any script is derived, so nothing downstream saw ` +
319
+ `this value; without it the failure surfaces inside Evolution's CBOR encoder ` +
320
+ `naming neither the field, the record, nor the file.`);
321
+ }
322
+ return recorded;
323
+ }
324
+ /** A non-string value, described by type rather than by a misleading cast. */
325
+ function describe(value) {
326
+ if (value === null)
327
+ return "null";
328
+ if (value === undefined)
329
+ return "undefined";
330
+ const ctor = value?.constructor?.name;
331
+ return `a ${typeof value}${ctor && ctor !== typeof value ? ` (${ctor})` : ``}: ` +
332
+ `${String(value).slice(0, 80)}`;
333
+ }
334
+ function refuseOnUnfrackingParameter(recorded, derivedUnfrackingHash, blueprintTitle) {
335
+ if (recorded === derivedUnfrackingHash || recorded === UNFRACKING_DISABLED)
336
+ return;
337
+ const shown = JSON.stringify(recorded);
338
+ throw new DeploymentMismatchError([], blueprintTitle, `programmableLogicGlobal.unfrackingParameter records ${shown}, which is neither ` +
339
+ `value a deployment may record. It must be EXACTLY ONE OF:\n` +
340
+ ` ${derivedUnfrackingHash} — the derived unfracking script hash ` +
341
+ `(unfracking ENABLED: the dispatcher's unfracking arm dispatches to it)\n` +
342
+ ` ${UNFRACKING_DISABLED} — UNFRACKING_DISABLED ` +
343
+ `(unfracking deployed and published, but the dispatcher's unfracking arm made ` +
344
+ `permanently unsatisfiable)\n` +
345
+ `A third value is a typo, a truncation, or a hash from a different deployment. ` +
346
+ `It is not inferable and it is not defaultable: it is what this dispatcher was ` +
347
+ `COMPILED AGAINST, so a wrong value here means every transaction that withdraws ` +
348
+ `through programmable_logic_global was built for a script that is not on chain.`);
349
+ }
350
+ function deriveDeploymentScripts(blueprint, deployment) {
351
+ // ⛔ BEFORE ANY DERIVATION, AND READ EXACTLY ONCE. Two reads of one recorded
352
+ // field are two chances to disagree — this module's own founding lesson — and
353
+ // a guard placed after the first read would be guarding a value the builder
354
+ // had already consumed. `readUnfrackingParameter` refuses an absent or
355
+ // inherited property and a malformed one; `refuseOnUnfrackingParameter` (run
356
+ // after the hash checks, see the call sites) refuses a well-formed value that
357
+ // is not one of the two this deployment may record.
358
+ //
359
+ // ⚠ IT VALIDATES, IT DOES NOT TRANSFORM. The string handed to the builder
360
+ // below is the same string the record holds — no trim, no lowercase, no
361
+ // 0x-strip — so this guard cannot move a single derived hash.
362
+ const recordedUnfrackingParameter = readUnfrackingParameter(deployment, blueprint.preamble.title);
165
363
  const builders = createStandardScripts(blueprint);
364
+ const plb = deployment.programmableLogicBase.scriptHash;
365
+ const registryPolicy = deployment.registry.scriptHash;
366
+ const paramsPolicy = deployment.protocolParams.policyId;
367
+ const mid = deployment.maxInlineDatumBytes;
368
+ // ⚑ ONE call per script. Every `derived` field below reads a `.hash` off one
369
+ // of these objects, and `scripts` returns the very same objects — so a wrong
370
+ // argument here is caught by the check beside it rather than surviving into
371
+ // the resolved surface.
372
+ const protocolParams = builders.protocolParams(deployment.protocolParams.txInput);
373
+ const programmableLogicBase = builders.programmableLogicBase(paramsPolicy);
374
+ const transfer = builders.transfer(plb, registryPolicy, mid);
375
+ const thirdParty = builders.thirdParty(plb, registryPolicy, mid);
376
+ const unfracking = builders.unfracking(plb, registryPolicy, mid);
377
+ // ⛔ THE THIRD ARGUMENT IS READ FROM THE RECORD, NOT FROM `unfracking`. A
378
+ // deployment may compile this dispatcher against `UNFRACKING_DISABLED` while
379
+ // deploying, publishing and recording the REAL unfracking script beside it,
380
+ // so `deployment.unfracking.scriptHash` is NOT the value that was hashed —
381
+ // only `programmableLogicGlobal.unfrackingParameter` says what was. Reading
382
+ // the wrong one derives a dispatcher hash for a script nobody deployed, and
383
+ // the failure surfaces at withdrawal time naming neither field.
384
+ //
385
+ // ⚠ It is used unvalidated HERE and refused by `refuseOnUnfrackingParameter`
386
+ // before either public entry point returns — see the call sites for why the
387
+ // refusal is sequenced after the hash mismatches rather than before them.
388
+ const programmableLogicGlobal = builders.programmableLogicGlobal(deployment.transfer.scriptHash, deployment.thirdParty.scriptHash, recordedUnfrackingParameter);
389
+ const issuanceCborHexMint = builders.issuanceCborHexMint(deployment.issuance.txInput, deployment.issuance.alwaysFailScriptHash);
390
+ const registry = builders.registry(deployment.registry.txInput, deployment.registry.issuanceScriptHash);
391
+ const issuanceLogic = builders.issuanceLogic(plb, registryPolicy, paramsPolicy, mid);
392
+ const upgradeMultisig = builders.upgradeMultisig(deployment.upgradeMultisig.txInput);
166
393
  const checks = [
167
394
  {
168
- name: "coordination_spend",
169
- derived: builders.coordinationSpend(deployment.coordinationNonce).hash,
170
- deployed: deployment.coordination.scriptHash,
171
- },
172
- {
173
- name: "protocol_params_mint",
174
- derived: builders.protocolParamsMint(deployment.protocolParams.txInput, deployment.protocolParams.coordinationScriptHash).hash,
395
+ // ⚑ ONE derivation for what used to be two. The params NFT policy and the
396
+ // params address's payment credential are the same value in alpha.3, so
397
+ // deriving them separately is not "belt and braces" — it is two chances
398
+ // to disagree about one fact.
399
+ name: "protocol_params (policy == address)",
400
+ derived: protocolParams.hash,
175
401
  deployed: deployment.protocolParams.policyId,
176
402
  },
177
403
  {
178
404
  name: "programmable_logic_base",
179
- derived: builders.programmableLogicBase(deployment.protocolParams.policyId).hash,
180
- deployed: deployment.programmableLogicBase.scriptHash,
405
+ derived: programmableLogicBase.hash,
406
+ deployed: plb,
181
407
  },
182
408
  {
183
409
  name: "transfer",
184
- derived: builders.transfer(deployment.protocolParams.policyId).hash,
410
+ derived: transfer.hash,
185
411
  deployed: deployment.transfer.scriptHash,
186
412
  },
187
413
  {
188
414
  name: "third_party",
189
- derived: builders.thirdParty(deployment.protocolParams.policyId).hash,
415
+ derived: thirdParty.hash,
190
416
  deployed: deployment.thirdParty.scriptHash,
191
417
  },
192
418
  {
193
419
  name: "unfracking",
194
- derived: builders.unfracking(deployment.protocolParams.policyId).hash,
420
+ derived: unfracking.hash,
195
421
  deployed: deployment.unfracking.scriptHash,
196
422
  },
423
+ {
424
+ // ⛔ THE COHERENCE CHECK THE LEDGER CANNOT DO. A script cannot read
425
+ // another script's parameters, so nothing on chain verifies that the
426
+ // dispatcher was compiled against THESE three delegates. Deriving it from
427
+ // the deployment's own delegate hashes is the only place that mismatch
428
+ // can be caught — and a dispatcher naming stale delegates fails at
429
+ // withdrawal time with an index error, never with "wrong dispatcher".
430
+ //
431
+ // ⚠ AND IT IS NOW REACHABLE FROM THREE CAUSES, NOT ONE. Its name says
432
+ // "stale delegates", but the third derivation input is the RECORDED
433
+ // `programmableLogicGlobal.unfrackingParameter` — so a record that
434
+ // compiled against `UNFRACKING_DISABLED` and then wrote the enabled
435
+ // dispatcher's hash (or the reverse) fails HERE, with a name pointing at
436
+ // the two fields that are fine. The parameter is also the only one of the
437
+ // three that cannot be cross-checked against anything else in the record.
438
+ // `dispatcherCauseHint` names all three in the thrown message; do not let
439
+ // this comment and that message drift apart.
440
+ name: PLG_CHECK_NAME,
441
+ derived: programmableLogicGlobal.hash,
442
+ deployed: deployment.programmableLogicGlobal.scriptHash,
443
+ },
197
444
  {
198
445
  name: "issuance_cbor_hex_mint",
199
- derived: builders.issuanceCborHexMint(deployment.issuance.txInput, deployment.issuance.alwaysFailScriptHash).hash,
446
+ derived: issuanceCborHexMint.hash,
200
447
  deployed: deployment.issuance.policyId,
201
448
  },
202
449
  {
203
- name: "registry_spend",
204
- derived: builders.registrySpend(deployment.protocolParams.policyId).hash,
205
- deployed: deployment.directorySpend.scriptHash,
450
+ // ⚑ Also one derivation for what used to be two (registry_mint's policy
451
+ // and registry_spend's address).
452
+ name: "registry (policy == address)",
453
+ derived: registry.hash,
454
+ deployed: registryPolicy,
206
455
  },
207
456
  {
208
- name: "registry_mint",
209
- derived: builders.registryMint(deployment.directoryMint.txInput, deployment.issuance.policyId, deployment.directorySpend.scriptHash).hash,
210
- deployed: deployment.directoryMint.scriptHash,
457
+ // ⛔ THE FOURTH CONSUMER OF `mid`. transfer, third_party and unfracking
458
+ // were three; alpha.4's issuance_logic is the fourth, and the count is
459
+ // the mechanism: the negative test asserting FOUR mismatches for a wrong
460
+ // `maxInlineDatumBytes` is what proves this call site exists. A comment
461
+ // cannot prove a call site; a count can.
462
+ //
463
+ // ⚠ Argument order is `(plb, registry_node_cs, params_policy, mid)` and
464
+ // the middle two are both bare PolicyIds. Swapping them builds, hashes
465
+ // and deploys — see the adjacent-parameter negative in
466
+ // test/deployment-assertion.test.mjs.
467
+ name: "issuance_logic",
468
+ derived: issuanceLogic.hash,
469
+ deployed: deployment.issuanceLogic.scriptHash,
470
+ },
471
+ {
472
+ // ⛔ CHECKED AGAIN, AND THE REASON IT CAN BE IS THE WHOLE CHANGE. This
473
+ // check was REMOVED in S-11 after it shipped a defect: it derived
474
+ // `upgrade_multisig` from `upgradeAuthority.hash`, a relationship that
475
+ // never existed (a PAYMENT key hash matched against `extra_signatories`
476
+ // versus the STAKE credential named in the params datum). alpha.3 could
477
+ // not derive it at all — its signer set and threshold were a deployment
478
+ // CHOICE that DeploymentParams did not record. alpha.4 replaces both
479
+ // parameters with a single `utxo_ref`, which IS recorded, so the hash is
480
+ // derivable and the check comes back.
481
+ //
482
+ // ⚠ AND THE VACUITY TRAP COMES BACK WITH IT, IN A NEW SHAPE. What hid the
483
+ // S-11 bug was a fixture using ONE value for two fields, so the wrong
484
+ // derivation reproduced perfectly and the check passed vacuously — only a
485
+ // live devnet exposed it. DeploymentParams now holds TWO one-shot
486
+ // `TxInput`s of identical type: `protocolParams.txInput` and
487
+ // `upgradeMultisig.txInput`. A fixture that reuses one for both makes
488
+ // this check pass no matter which one the code reads.
489
+ //
490
+ // ⛔ Do NOT add any check relating `upgradeAuthority` to
491
+ // `upgradeMultisig`. A deployment may legitimately name a key credential,
492
+ // a different script, or the multisig as its authority, and the validator
493
+ // never inspects it. Such a check would reject valid deployments — it is
494
+ // the S-11 defect returning under a new name.
495
+ name: "upgrade_multisig",
496
+ derived: upgradeMultisig.hash,
497
+ deployed: deployment.upgradeMultisig.scriptHash,
211
498
  },
212
499
  ];
500
+ return {
501
+ builders,
502
+ paramsPolicy,
503
+ unfrackingParameter: {
504
+ recorded: recordedUnfrackingParameter,
505
+ derivedUnfrackingHash: unfracking.hash,
506
+ },
507
+ scripts: {
508
+ protocolParams,
509
+ programmableLogicBase,
510
+ transfer,
511
+ thirdParty,
512
+ unfracking,
513
+ programmableLogicGlobal,
514
+ issuanceCborHexMint,
515
+ registry,
516
+ issuanceLogic,
517
+ upgradeMultisig,
518
+ },
519
+ checks,
520
+ };
521
+ }
522
+ /** Throw if any derived hash disagrees with the deployment record. */
523
+ function refuseOnMismatch(checks, blueprintTitle) {
213
524
  const mismatches = checks.filter((c) => c.derived !== c.deployed);
214
525
  if (mismatches.length > 0) {
215
- throw new DeploymentMismatchError(mismatches, blueprint.preamble.title);
526
+ throw new DeploymentMismatchError(mismatches, blueprintTitle);
216
527
  }
528
+ }
529
+ export function assertDeploymentScripts(blueprint, deployment) {
530
+ const { checks, unfrackingParameter } = deriveDeploymentScripts(blueprint, deployment);
531
+ refuseOnMismatch(checks, blueprint.preamble.title);
532
+ // ⚠ ORDER IS DELIBERATE, AND IT IS NOT A CONDITIONAL. A wrong
533
+ // `maxInlineDatumBytes` moves the derived `unfracking` hash, which would let a
534
+ // parameter complaint pre-empt — and hide — the four named delegate
535
+ // mismatches that say what actually went wrong. Hash disagreements are
536
+ // reported first, by name; this refusal is what is left when all ten
537
+ // reproduce and the record is nevertheless not a legal deployment.
538
+ refuseOnUnfrackingParameter(unfrackingParameter.recorded, unfrackingParameter.derivedUnfrackingHash, blueprint.preamble.title);
217
539
  return checks;
218
540
  }
219
541
  /**
@@ -225,21 +547,30 @@ export function assertDeploymentScripts(blueprint, deployment) {
225
547
  * undetectable until submission.
226
548
  */
227
549
  export function buildDeploymentScripts(blueprint, deployment) {
228
- assertDeploymentScripts(blueprint, deployment);
229
- const builders = createStandardScripts(blueprint);
230
- const paramsPolicy = deployment.protocolParams.policyId;
550
+ // ⚑ ONE FACT, ONE DERIVATION — and this line is what makes that phrase true
551
+ // rather than aspirational. The scripts returned below are the SAME OBJECTS
552
+ // whose hashes were just checked, not a second build from the same arguments.
553
+ // A second build is a second chance to disagree, and it WAS unguarded here:
554
+ // see the block on `DerivedDeployment`.
555
+ const { builders, paramsPolicy, scripts, checks, unfrackingParameter } = deriveDeploymentScripts(blueprint, deployment);
556
+ refuseOnMismatch(checks, blueprint.preamble.title);
557
+ // Same refusal, same order, as `assertDeploymentScripts` — the resolved
558
+ // surface must never be built from a parameter the assertion would reject.
559
+ refuseOnUnfrackingParameter(unfrackingParameter.recorded, unfrackingParameter.derivedUnfrackingHash, blueprint.preamble.title);
231
560
  return {
232
- coordinationSpend: builders.coordinationSpend(deployment.coordinationNonce),
233
- protocolParamsMint: builders.protocolParamsMint(deployment.protocolParams.txInput, deployment.protocolParams.coordinationScriptHash),
234
- programmableLogicBase: builders.programmableLogicBase(paramsPolicy),
235
- transfer: builders.transfer(paramsPolicy),
236
- thirdParty: builders.thirdParty(paramsPolicy),
237
- unfracking: builders.unfracking(paramsPolicy),
238
- issuanceCborHexMint: builders.issuanceCborHexMint(deployment.issuance.txInput, deployment.issuance.alwaysFailScriptHash),
239
- registryMint: builders.registryMint(deployment.directoryMint.txInput, deployment.issuance.policyId, deployment.directorySpend.scriptHash),
240
- registrySpend: builders.registrySpend(paramsPolicy),
561
+ ...scripts,
241
562
  buildIssuanceMint(mintingLogicHash) {
242
- return builders.issuanceMint(deployment.programmableLogicBase.scriptHash, deployment.directoryMint.scriptHash, mintingLogicHash, paramsPolicy);
563
+ // ⚑ ITS SIGNATURE IS UNCHANGED AND ITS RESULT IS NOT. alpha.4 dropped
564
+ // `programmable_logic_base` and `registry_node_cs` from issuance_mint's
565
+ // parameters, so THE POLICY ID CHANGES FOR THE SAME MINTING LOGIC. That
566
+ // is correct, not a bug to fix: an alpha.3 token and an alpha.4 token
567
+ // built from identical issuance logic are different assets.
568
+ //
569
+ // ⚠ NOT part of the single-derivation collapse above, and cannot be: it
570
+ // is parameterised per minting logic, so there is no one hash for
571
+ // `assertDeploymentScripts` to check. It stays uncovered, as the
572
+ // assertion's own "Not covered" note says.
573
+ return builders.issuanceMint(mintingLogicHash, paramsPolicy);
243
574
  },
244
575
  };
245
576
  }