@easy1staking/cip113-sdk-ts 0.10.0 → 0.11.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.
- package/README.md +55 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -1
- package/dist/index.js.map +1 -1
- package/dist/standard/bootstrap.d.ts +552 -0
- package/dist/standard/bootstrap.d.ts.map +1 -0
- package/dist/standard/bootstrap.js +1077 -0
- package/dist/standard/bootstrap.js.map +1 -0
- package/dist/substandards/address-guard.d.ts +60 -0
- package/dist/substandards/address-guard.d.ts.map +1 -0
- package/dist/substandards/address-guard.js +159 -0
- package/dist/substandards/address-guard.js.map +1 -0
- package/dist/substandards/dummy/index.d.ts.map +1 -1
- package/dist/substandards/dummy/index.js +32 -5
- package/dist/substandards/dummy/index.js.map +1 -1
- package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
- package/dist/substandards/freeze-and-seize/index.js +45 -9
- package/dist/substandards/freeze-and-seize/index.js.map +1 -1
- package/dist/substandards/interface.d.ts +20 -2
- package/dist/substandards/interface.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol bootstrap — the transactions that stand up a CIP-113 instance.
|
|
3
|
+
*
|
|
4
|
+
* TARGETS CIP-113 0.5.0-alpha.4 (upstream 7e8a63198c5b240135f1aa2f043ce5d7c046b2c4).
|
|
5
|
+
*
|
|
6
|
+
* ⚑ WHY THIS IS EXPORTED AT ALL. Until 2026-09-17 this sequence lived only in
|
|
7
|
+
* `test/harness/bootstrap.ts`, which `files: ["dist","blueprints"]` does not
|
|
8
|
+
* ship — so the platform maintained ITS OWN PORT of a protocol-critical
|
|
9
|
+
* sequence. That port had already diverged (it reserves the seed UTxOs from
|
|
10
|
+
* coin selection where the harness did not) and the divergence was CORRECT.
|
|
11
|
+
* Two implementations, one right and the other not yet wrong, is worse than one
|
|
12
|
+
* supported export. See CLAUDE.md, "Protocol bootstrap — AMENDED 2026-09-17",
|
|
13
|
+
* and `design/bootstrap-export.md` for the step boundaries and their reasons.
|
|
14
|
+
*
|
|
15
|
+
* ⛔ WHAT THIS MODULE DOES NOT DO, AND WILL NOT. It does not sign, submit, await,
|
|
16
|
+
* poll, fund, retry, or hold a key. It builds unsigned transactions and returns
|
|
17
|
+
* them. Orchestration is the caller's, and that is not a gap — it is what lets
|
|
18
|
+
* the platform keep its own UTxO reservation and its own confirmation strategy.
|
|
19
|
+
*
|
|
20
|
+
* ⛔ AND IT SHIPS NO FIXTURE VALUES. No mnemonic, no endpoint, no nonce, no
|
|
21
|
+
* security parameter, no placeholder policy id, no `any`. A value the caller
|
|
22
|
+
* must decide is a REQUIRED INPUT, never a default. That rule is the whole
|
|
23
|
+
* reason this export is safe to make: everything the devnet fixture chose for
|
|
24
|
+
* itself is now something its CALL passes in.
|
|
25
|
+
*
|
|
26
|
+
* ---------------------------------------------------------------------------
|
|
27
|
+
* THE SEQUENCE IS DEPENDENT, SO THE EXPORT IS STEPWISE
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
*
|
|
30
|
+
* The one-shot minting policies are parameterised by SPECIFIC OUTPUT REFERENCES
|
|
31
|
+
* of an earlier transaction, so step N+1 cannot be built until step N has been
|
|
32
|
+
* submitted and its outputs observed. There is no "give me five transactions"
|
|
33
|
+
* shape available; asking for one would mean signing, and signing is out.
|
|
34
|
+
*
|
|
35
|
+
* 1 buildSeedTx -> three distinct seed UTxOs
|
|
36
|
+
* 2 buildMultisigGenesisTx -> the upgrade authority's config UTxO
|
|
37
|
+
* 3 buildProtocolGenesisTx -> params, registry origin, issuance CBOR
|
|
38
|
+
* 4 buildReferenceScriptsTx -> the seven reference scripts
|
|
39
|
+
* 5 buildStakeRegistrationTx -> the six withdraw-0 registrations
|
|
40
|
+
* assembleDeploymentParams -> DeploymentParams (pure)
|
|
41
|
+
*
|
|
42
|
+
* ⚑ RESUMPTION. `planBootstrap(config)` is pure and deterministic: persist the
|
|
43
|
+
* CONFIG and every hash, address, datum and asset unit can be re-derived on any
|
|
44
|
+
* machine with no chain access. No step needs an earlier step's CONSUMED
|
|
45
|
+
* inputs, which is the property that makes resuming at step N work at all — by
|
|
46
|
+
* the time you resume at step 4 the seeds are spent and unfetchable.
|
|
47
|
+
*/
|
|
48
|
+
import { Address as EvoAddress, Assets as EvoAssets, Bytes, Credential, Data, InlineDatum, Transaction as EvoTransaction, TransactionHash as EvoTransactionHash, UPLC, } from "@evolution-sdk/evolution";
|
|
49
|
+
import { buildCip171RecordFromPin } from "../core/provenance.js";
|
|
50
|
+
import { buildCip171Metadatum, CIP171_METADATA_LABEL } from "../core/cip171.js";
|
|
51
|
+
import { buildEvoScript, ceilToWholeAda, decodeMultisigScript, getInlineDatum, mintAssetsFromMap, minUtxoAtLeast, minUtxoForOutput, multisigScriptDatum, outputAssets, protocolParamsDatum, registryInitRedeemer, registryNodeDatum, REGISTRY_NODE_MIN_ADA, scriptAddress, stringToHex, voidData, } from "../core/evo-utils.js";
|
|
52
|
+
import { STANDARD_VALIDATORS, validateStandardBlueprint } from "./blueprint.js";
|
|
53
|
+
import { createStandardScripts, UNFRACKING_DISABLED, } from "./scripts.js";
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Constants that are PROTOCOL FACTS, not caller choices
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Three seed UTxOs, and the count is not negotiable.
|
|
59
|
+
*
|
|
60
|
+
* utxo1 -> protocol_params AND registry, utxo2 -> issuance_cbor_hex_mint,
|
|
61
|
+
* utxo3 -> upgrade_multisig.
|
|
62
|
+
*
|
|
63
|
+
* ⛔ THREE DISTINCT ONES, NOT ONE REUSED. Reusing utxo1 for upgrade_multisig
|
|
64
|
+
* deploys perfectly well — a one-shot outref is consumed once, and the two
|
|
65
|
+
* policies consume it in different transactions, so nothing on chain objects.
|
|
66
|
+
* The reason it must be distinct is OFF chain: `DeploymentParams` carries TWO
|
|
67
|
+
* same-typed one-shot outrefs, `protocolParams.txInput` and
|
|
68
|
+
* `upgradeMultisig.txInput`, and `assertDeploymentScripts` derives a hash from
|
|
69
|
+
* each. A record that gives them ONE value makes the upgrade_multisig check
|
|
70
|
+
* pass whichever field the code reads, so the check cannot fail and a real
|
|
71
|
+
* defect in which field is read is invisible. That is not hypothetical: it hid
|
|
72
|
+
* a wrong derivation for an entire migration once already.
|
|
73
|
+
*/
|
|
74
|
+
export const BOOTSTRAP_SEED_COUNT = 3;
|
|
75
|
+
/**
|
|
76
|
+
* The reference-script publication order, which is ONE FACT WRITTEN ONCE.
|
|
77
|
+
*
|
|
78
|
+
* ⛔ APPENDED, NEVER INSERTED. `buildReferenceScriptsTx` pays them in this
|
|
79
|
+
* order and `assembleDeploymentParams` derives every `…RefInput` index from the
|
|
80
|
+
* same array. alpha.3 inserted the dispatcher at index 1 and shifted all three
|
|
81
|
+
* delegates down — and a mismatch here does not fail loudly. It hands out a
|
|
82
|
+
* reference input carrying the WRONG script, and the transaction dies at
|
|
83
|
+
* evaluation naming neither.
|
|
84
|
+
*
|
|
85
|
+
* ⛔ AND APPENDING IS NOT FREE — THERE IS A CAP AND THIS LIST IS NEAR IT (F-9).
|
|
86
|
+
* MEASURED on devnet, 2026-09-17: the transaction publishing these seven comes
|
|
87
|
+
* to **12,496 bytes against a 16,384-byte protocol maximum — 76%**. Two more
|
|
88
|
+
* scripts of the size already here would burst it, and the failure arrives at
|
|
89
|
+
* SUBMISSION rather than at build. This is the same cap that forced the
|
|
90
|
+
* mint/publish/register split in the first place: the 0.3.x single-transaction
|
|
91
|
+
* fixture measured 21,816 bytes. `buildReferenceScriptsTx` now refuses over the
|
|
92
|
+
* chain's own `maxTxSize` rather than letting the ledger say it; when that
|
|
93
|
+
* refusal fires, the answer is a SECOND publish transaction and a second
|
|
94
|
+
* recorded tx hash, not a shorter list.
|
|
95
|
+
*/
|
|
96
|
+
export const REFERENCE_SCRIPT_ORDER = [
|
|
97
|
+
"programmableLogicBase",
|
|
98
|
+
"programmableLogicGlobal",
|
|
99
|
+
"transfer",
|
|
100
|
+
"thirdParty",
|
|
101
|
+
"unfracking",
|
|
102
|
+
"issuanceLogic",
|
|
103
|
+
"upgradeMultisig",
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* The six withdraw-0 credentials a bootstrap must register.
|
|
107
|
+
*
|
|
108
|
+
* ⛔ SIX, AND EVERY OMISSION IS UNDIAGNOSABLE. An unregistered stake credential
|
|
109
|
+
* does not fail with "not registered": the ledger answers code 3141, "rewards
|
|
110
|
+
* withdrawals must consume rewards in full", which reads as a balance problem
|
|
111
|
+
* and sends the reader to the wrong subsystem entirely. MEASURED on devnet —
|
|
112
|
+
* every programmable operation failed that way until the dispatcher joined this
|
|
113
|
+
* list, and `issuance_logic` rides EVERY mint and EVERY burn.
|
|
114
|
+
*/
|
|
115
|
+
export const STAKE_REGISTRATION_ORDER = [
|
|
116
|
+
"programmableLogicGlobal",
|
|
117
|
+
"transfer",
|
|
118
|
+
"thirdParty",
|
|
119
|
+
"unfracking",
|
|
120
|
+
"issuanceLogic",
|
|
121
|
+
"upgradeMultisig",
|
|
122
|
+
];
|
|
123
|
+
/** The five steps, in order. Exported so a caller can name where it resumed. */
|
|
124
|
+
export const BOOTSTRAP_STEPS = [
|
|
125
|
+
"seed",
|
|
126
|
+
"multisig-genesis",
|
|
127
|
+
"protocol-genesis",
|
|
128
|
+
"reference-scripts",
|
|
129
|
+
"stake-registrations",
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* A FLOOR for solved min-UTxO amounts, not a chosen output size.
|
|
133
|
+
*
|
|
134
|
+
* ⚠ Every datum-bearing output below is SOLVED with `minUtxoAtLeast`, never set
|
|
135
|
+
* flat: min-UTxO scales with serialised output size, and Evolution does NOT
|
|
136
|
+
* rescue an under-funded `payToAddress`. The shortfall survives to submission
|
|
137
|
+
* and is reported as "insufficient Ada", which sends the reader to the wallet
|
|
138
|
+
* balance rather than to the datum that grew. The floor only keeps the figure
|
|
139
|
+
* monotone against what this package already emitted; the solved value governs.
|
|
140
|
+
*/
|
|
141
|
+
const MIN_UTXO_FLOOR = 2000000n;
|
|
142
|
+
/**
|
|
143
|
+
* Solve min-UTxO for a genesis output, then ROUND UP TO A WHOLE ADA.
|
|
144
|
+
*
|
|
145
|
+
* ⛔ THE ROUNDING IS DELIBERATE AND IT IS NOT TIDINESS (F-7). `minUtxoAtLeast`
|
|
146
|
+
* returns the EXACT minimum, and its own docstring promises only that the
|
|
147
|
+
* figure "only ever rises" relative to what this package used to emit — it
|
|
148
|
+
* makes no promise about the ENCODER. The figure is
|
|
149
|
+
* `coinsPerUtxoByte × (160 + serialised output size)`, and the serialised size
|
|
150
|
+
* comes from Evolution's `TxOut` encoder, which this package does not own.
|
|
151
|
+
*
|
|
152
|
+
* ⚠ THE FAILURE THAT BUYS: a one-byte widening in that encoder — a version
|
|
153
|
+
* bump, a CBOR canonicalisation change — under-funds an output by
|
|
154
|
+
* `coinsPerUtxoByte` lovelace. Evolution does NOT rescue an under-funded
|
|
155
|
+
* `payToAddress`: the shortfall survives to submission and the ledger rejects
|
|
156
|
+
* it as "insufficient Ada", which sends the reader to the wallet balance. On
|
|
157
|
+
* the protocol genesis that rejection lands on an IRREVERSIBLE step, after the
|
|
158
|
+
* multisig config UTxO already exists on chain.
|
|
159
|
+
*
|
|
160
|
+
* ⇒ A whole-ADA ceiling is a RULE, not a guessed constant, and at
|
|
161
|
+
* `coinsPerUtxoByte` 4310 it absorbs on the order of a hundred bytes of encoder
|
|
162
|
+
* drift on the largest output here. It is still an order of magnitude below the
|
|
163
|
+
* flat 15 ADA this sequence used to write.
|
|
164
|
+
*/
|
|
165
|
+
function genesisOutputLovelace(params) {
|
|
166
|
+
return ceilToWholeAda(minUtxoAtLeast(MIN_UTXO_FLOOR, params));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The lowest lovelace an output of this shape can legally carry — a FLOOR to
|
|
170
|
+
* refuse below, never a sufficiency check.
|
|
171
|
+
*
|
|
172
|
+
* ⚠ Clearing min-UTxO is necessary and not sufficient: a seed output also has
|
|
173
|
+
* to fund the fee of the transaction that consumes it, and this says nothing
|
|
174
|
+
* about that.
|
|
175
|
+
*/
|
|
176
|
+
function minLovelaceForPlainOutput(address, coinsPerUtxoByte) {
|
|
177
|
+
return minUtxoForOutput({ address, assets: outputAssets(0n), coinsPerUtxoByte });
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The 28-byte marker `issuance_mint` is parameterised against so the stored
|
|
181
|
+
* CBOR can be cut either side of its minting-logic argument.
|
|
182
|
+
*
|
|
183
|
+
* ⛔ NOT A PARAMETER, NOT A DEFAULT, AND NEVER DEPLOYED. It is applied and
|
|
184
|
+
* immediately cut back out; what reaches the chain is `pre` and `post` with the
|
|
185
|
+
* marker gone from between them, so that a later registration can splice a REAL
|
|
186
|
+
* minting-logic hash in without re-deriving the script. Passing this value
|
|
187
|
+
* anywhere a minting-logic hash is expected would produce a policy nobody can
|
|
188
|
+
* mint under — it is a cutting guide, not an identifier.
|
|
189
|
+
*
|
|
190
|
+
* ⚑ EXPORTED SO THE OCCURRENCE GUARD CAN BE EXERCISED, and for no other reason.
|
|
191
|
+
* `splitParts.length !== 2` below is the entire reason shipping a fixed hex
|
|
192
|
+
* marker is tolerable — and an unmutated guard is a claim rather than a check.
|
|
193
|
+
* A test cannot construct a body that collides with a marker it cannot see, so
|
|
194
|
+
* the marker is visible. (MEASURED: the `pre`/`post` halves do not depend on
|
|
195
|
+
* this VALUE — two different markers yield byte-identical halves, because the
|
|
196
|
+
* argument occupies the same bit positions either way.)
|
|
197
|
+
*/
|
|
198
|
+
export const ISSUANCE_SPLICE_MARKER = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeef";
|
|
199
|
+
/**
|
|
200
|
+
* The three withdraw-0 delegates whose registration runs a `publish` handler.
|
|
201
|
+
*
|
|
202
|
+
* Registering emits a Conway RegCert, which executes the script under the
|
|
203
|
+
* PUBLISH purpose — so each needs a publish handler or the transaction dies at
|
|
204
|
+
* evaluation with a bare "machine terminated" and an EMPTY trace list. The
|
|
205
|
+
* check is kept because the failure it diagnoses is undiagnosable without it.
|
|
206
|
+
*/
|
|
207
|
+
const REQUIRED_PUBLISH_HANDLERS = [
|
|
208
|
+
"transfer.transfer.publish",
|
|
209
|
+
"third_party.third_party.publish",
|
|
210
|
+
"unfracking.unfracking.publish",
|
|
211
|
+
];
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Required-input refusals — by name, and before anything is derived
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
function requireHex(value, field, bytes) {
|
|
216
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
217
|
+
throw new Error(`bootstrap: ${field} is required and must be a non-empty hex string, got ` +
|
|
218
|
+
`${value === undefined ? "undefined" : JSON.stringify(value)}. This SDK ships no ` +
|
|
219
|
+
`default for it — it is a value the caller must decide.`);
|
|
220
|
+
}
|
|
221
|
+
if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {
|
|
222
|
+
throw new Error(`bootstrap: ${field} must be an even-length hex string with no 0x prefix, got ` +
|
|
223
|
+
`${JSON.stringify(value)}.`);
|
|
224
|
+
}
|
|
225
|
+
if (bytes !== undefined && value.length !== bytes * 2) {
|
|
226
|
+
throw new Error(`bootstrap: ${field} must be exactly ${bytes} bytes (${bytes * 2} hex chars), got ` +
|
|
227
|
+
`${value.length / 2}.`);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function requireTxInput(value, field) {
|
|
232
|
+
const ref = value;
|
|
233
|
+
if (!ref || typeof ref !== "object") {
|
|
234
|
+
throw new Error(`bootstrap: ${field} is required — a { txHash, outputIndex } naming the one-shot UTxO ` +
|
|
235
|
+
`this policy is parameterised by. Got ${JSON.stringify(value)}.`);
|
|
236
|
+
}
|
|
237
|
+
requireHex(ref.txHash, `${field}.txHash`, 32);
|
|
238
|
+
if (!Number.isInteger(ref.outputIndex) || ref.outputIndex < 0) {
|
|
239
|
+
throw new Error(`bootstrap: ${field}.outputIndex must be a non-negative integer, got ` +
|
|
240
|
+
`${JSON.stringify(ref.outputIndex)}.`);
|
|
241
|
+
}
|
|
242
|
+
return { txHash: ref.txHash.toLowerCase(), outputIndex: ref.outputIndex };
|
|
243
|
+
}
|
|
244
|
+
const refKey = (ref) => `${ref.txHash.toLowerCase()}#${ref.outputIndex}`;
|
|
245
|
+
function requireSeeds(seeds) {
|
|
246
|
+
const s = seeds;
|
|
247
|
+
if (!s || typeof s !== "object") {
|
|
248
|
+
throw new Error(`bootstrap: seeds is required — three DISTINCT one-shot output references, as ` +
|
|
249
|
+
`{ protocolParams, issuance, upgradeMultisig }. Got ${JSON.stringify(seeds)}.`);
|
|
250
|
+
}
|
|
251
|
+
const resolved = {
|
|
252
|
+
protocolParams: requireTxInput(s.protocolParams, "seeds.protocolParams"),
|
|
253
|
+
issuance: requireTxInput(s.issuance, "seeds.issuance"),
|
|
254
|
+
upgradeMultisig: requireTxInput(s.upgradeMultisig, "seeds.upgradeMultisig"),
|
|
255
|
+
};
|
|
256
|
+
// ⛔ DISTINCTNESS IS A REFUSAL, NOT A WARNING. See BOOTSTRAP_SEED_COUNT: two
|
|
257
|
+
// roles sharing one outref deploy fine and make the off-chain derivation
|
|
258
|
+
// check for the shared pair VACUOUS. A check that cannot fail is worse than
|
|
259
|
+
// an absent one, because it reads as coverage.
|
|
260
|
+
const seen = new Map();
|
|
261
|
+
for (const [role, ref] of Object.entries(resolved)) {
|
|
262
|
+
const key = refKey(ref);
|
|
263
|
+
const previous = seen.get(key);
|
|
264
|
+
if (previous) {
|
|
265
|
+
throw new Error(`bootstrap: seeds.${previous} and seeds.${role} are the SAME output reference ` +
|
|
266
|
+
`(${key}). The three one-shot policies must be parameterised by three DISTINCT ` +
|
|
267
|
+
`UTxOs: DeploymentParams carries protocolParams.txInput and upgradeMultisig.txInput ` +
|
|
268
|
+
`as two same-typed fields, and a record that gives them one value makes ` +
|
|
269
|
+
`assertDeploymentScripts pass whichever field the code reads — so the check cannot ` +
|
|
270
|
+
`fail and a real defect in which field is read becomes invisible.`);
|
|
271
|
+
}
|
|
272
|
+
seen.set(key, role);
|
|
273
|
+
}
|
|
274
|
+
return resolved;
|
|
275
|
+
}
|
|
276
|
+
function requirePublishHandlers(blueprint) {
|
|
277
|
+
const titles = blueprint.validators.map((v) => v.title);
|
|
278
|
+
const missing = REQUIRED_PUBLISH_HANDLERS.filter((t) => !titles.includes(t));
|
|
279
|
+
if (missing.length > 0) {
|
|
280
|
+
throw new Error(`This blueprint cannot bootstrap a protocol: it lacks publish handler(s) ` +
|
|
281
|
+
`${missing.join(", ")}, so registering the corresponding stake credential fails at ` +
|
|
282
|
+
`script evaluation (purpose "publish") with no diagnostic.\n` +
|
|
283
|
+
`Blueprint: "${blueprint.preamble.title}" v${blueprint.preamble.version}.\n` +
|
|
284
|
+
`Present handlers: ${titles.filter((t) => t.endsWith(".publish")).join(", ") || "(none)"}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Extract the PlutusV3 script body hex (inner UPLC, no outer CBOR wrap). */
|
|
288
|
+
function scriptBodyHex(compiledCode) {
|
|
289
|
+
const level = UPLC.getCborEncodingLevel(compiledCode);
|
|
290
|
+
if (level !== "double")
|
|
291
|
+
return compiledCode;
|
|
292
|
+
const raw = Bytes.fromHex(compiledCode);
|
|
293
|
+
const additionalInfo = raw[0] & 0x1f;
|
|
294
|
+
const headerLen = additionalInfo < 24 ? 1 : additionalInfo === 24 ? 2 : additionalInfo === 25 ? 3 : 5;
|
|
295
|
+
return Bytes.toHex(raw.slice(headerLen));
|
|
296
|
+
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// planBootstrap
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
/**
|
|
301
|
+
* Derive a whole protocol instance from a config. Pure: no client, no network,
|
|
302
|
+
* no clock, no filesystem.
|
|
303
|
+
*
|
|
304
|
+
* ⚠ THE PARAMETERISATION ORDER IS LOAD-BEARING and each script's hash feeds the
|
|
305
|
+
* next. It is NOT a single chain — `programmable_logic_base` hangs off the
|
|
306
|
+
* params-NFT policy and everything downstream fans out — but the edges that do
|
|
307
|
+
* exist cannot be reordered. See `scripts.ts` for the full graph.
|
|
308
|
+
*
|
|
309
|
+
* ⛔ PARAMETER TYPES ARE NOT INTERCHANGEABLE AND TYPESCRIPT CANNOT TELL THEM
|
|
310
|
+
* APART: every one is a hex string at the call site. `issuance_logic` in
|
|
311
|
+
* particular takes TWO ADJACENT PolicyIds — `registry_node_cs` then
|
|
312
|
+
* `params_policy` — and swapping them yields a script that builds, hashes,
|
|
313
|
+
* deploys and is registered without a murmur. It simply looks for the registry
|
|
314
|
+
* under the params policy for the rest of its life.
|
|
315
|
+
*/
|
|
316
|
+
export function planBootstrap(config) {
|
|
317
|
+
if (!config || typeof config !== "object") {
|
|
318
|
+
throw new Error("planBootstrap: a BootstrapConfig is required.");
|
|
319
|
+
}
|
|
320
|
+
const { blueprint } = config;
|
|
321
|
+
if (!blueprint || typeof blueprint !== "object" || !Array.isArray(blueprint.validators)) {
|
|
322
|
+
throw new Error("planBootstrap: blueprint is required — the CIP-57 standard blueprint this instance is " +
|
|
323
|
+
"built from. Import it via the package's \"./blueprints/*\" export path.");
|
|
324
|
+
}
|
|
325
|
+
validateStandardBlueprint(blueprint);
|
|
326
|
+
requirePublishHandlers(blueprint);
|
|
327
|
+
if (typeof config.networkId !== "number" || !Number.isInteger(config.networkId)) {
|
|
328
|
+
throw new Error(`planBootstrap: networkId is required and must be an integer (0 for any testnet, 1 for ` +
|
|
329
|
+
`mainnet), got ${JSON.stringify(config.networkId)}. Every address this plan derives ` +
|
|
330
|
+
`depends on it, and a wrong one yields addresses that are valid and unreachable.`);
|
|
331
|
+
}
|
|
332
|
+
const seeds = requireSeeds(config.seeds);
|
|
333
|
+
const alwaysFailNonce = requireHex(config.alwaysFailNonce, "alwaysFailNonce");
|
|
334
|
+
if (typeof config.maxInlineDatumBytes !== "bigint" || config.maxInlineDatumBytes <= 0n) {
|
|
335
|
+
throw new Error(`planBootstrap: maxInlineDatumBytes is required and must be a positive bigint, got ` +
|
|
336
|
+
`${JSON.stringify(String(config.maxInlineDatumBytes))}. It is a SECURITY PARAMETER ` +
|
|
337
|
+
`baked into the hashes of transfer, third_party, unfracking and issuance_logic, and ` +
|
|
338
|
+
`this package deliberately ships no default for it — upstream gives no guidance and ` +
|
|
339
|
+
`the devnet fixture's 1024 is explicitly not a recommendation.`);
|
|
340
|
+
}
|
|
341
|
+
if (config.unfracking !== "enabled" && config.unfracking !== "disabled") {
|
|
342
|
+
throw new Error(`planBootstrap: unfracking is required and must be "enabled" or "disabled", got ` +
|
|
343
|
+
`${JSON.stringify(config.unfracking)}. The choice is baked into the dispatcher's hash, ` +
|
|
344
|
+
`so two deployments differing only here are DIFFERENT PROTOCOLS — it cannot be defaulted.`);
|
|
345
|
+
}
|
|
346
|
+
const events = [];
|
|
347
|
+
const builders = createStandardScripts(blueprint, (e) => events.push(e));
|
|
348
|
+
const max = config.maxInlineDatumBytes;
|
|
349
|
+
const alwaysFail = builders.alwaysFail(alwaysFailNonce);
|
|
350
|
+
// Depends on nothing but its own seed, so it can be built first — and its
|
|
351
|
+
// config UTxO must be minted BEFORE the protocol genesis names it. See
|
|
352
|
+
// buildMultisigGenesisTx for why that ordering is not a style choice.
|
|
353
|
+
const upgradeMultisig = builders.upgradeMultisig(seeds.upgradeMultisig);
|
|
354
|
+
// NO NONCE AND NO ORDERING EDGE. protocol_params takes only its one-shot
|
|
355
|
+
// utxo_ref: the mint side no longer depends on a spend-side address, so the
|
|
356
|
+
// cycle that once forced a coordination script to be built first is
|
|
357
|
+
// dissolved. Its hash is BOTH the NFT policy id and the address payment
|
|
358
|
+
// credential — the minting policy naming itself.
|
|
359
|
+
const protocolParams = builders.protocolParams(seeds.protocolParams);
|
|
360
|
+
const paramsPolicy = protocolParams.hash;
|
|
361
|
+
const programmableLogicBase = builders.programmableLogicBase(paramsPolicy);
|
|
362
|
+
// The registry does not touch the params chain at all — it reads its own
|
|
363
|
+
// policy off its own input's payment credential — so it can be built as soon
|
|
364
|
+
// as issuance_cbor_hex_mint exists. Its hash is also BOTH policy and address.
|
|
365
|
+
const issuanceCborHexMint = builders.issuanceCborHexMint(seeds.issuance, alwaysFail.hash);
|
|
366
|
+
const registry = builders.registry(seeds.protocolParams, issuanceCborHexMint.hash);
|
|
367
|
+
// ⚠ progLogicCred is programmable_logic_base's hash, NOT the dispatcher's.
|
|
368
|
+
// The dispatcher is parameterised BY these three, so the reverse would be a
|
|
369
|
+
// parameter cycle: a value derived from your own script hash can never be
|
|
370
|
+
// your own parameter.
|
|
371
|
+
const transfer = builders.transfer(programmableLogicBase.hash, registry.hash, max);
|
|
372
|
+
const thirdParty = builders.thirdParty(programmableLogicBase.hash, registry.hash, max);
|
|
373
|
+
const unfracking = builders.unfracking(programmableLogicBase.hash, registry.hash, max);
|
|
374
|
+
const unfrackingParameter = config.unfracking === "enabled" ? unfracking.hash : UNFRACKING_DISABLED;
|
|
375
|
+
// Built LAST of the delegate family: it names all three at compile time, so
|
|
376
|
+
// replacing ONE delegate requires deploying a new dispatcher too.
|
|
377
|
+
const programmableLogicGlobal = builders.programmableLogicGlobal(transfer.hash, thirdParty.hash, unfrackingParameter);
|
|
378
|
+
// ⛔ PARAMETERS 2 AND 3 ARE TWO ADJACENT PolicyIds AND BOTH ARE `string`:
|
|
379
|
+
// 1 progLogicCred = programmable_logic_base (NOT the dispatcher)
|
|
380
|
+
// 2 registryPolicy <- registry_node_cs
|
|
381
|
+
// 3 paramsPolicy <- params_policy
|
|
382
|
+
// 4 maxInlineDatumBytes
|
|
383
|
+
// Nothing — not the compiler, not the parameteriser, not the hash — can tell
|
|
384
|
+
// a swap from the intended order.
|
|
385
|
+
const issuanceLogic = builders.issuanceLogic(programmableLogicBase.hash, registry.hash, paramsPolicy, max);
|
|
386
|
+
// issuance_mint is parameterised per minting logic, which is not known until
|
|
387
|
+
// a token is registered. Build it once against a marker and store the CBOR
|
|
388
|
+
// either side of it, so registration can splice in the real hash without
|
|
389
|
+
// re-deriving the whole script.
|
|
390
|
+
//
|
|
391
|
+
// ⚑ TWO ARGUMENTS, CREDENTIAL FIRST: `(mintingLogicHash, paramsPolicy)`. The
|
|
392
|
+
// delegate credentials it used to be compiled against now reach it through
|
|
393
|
+
// the params datum instead — which is the whole point of the alpha.4 split.
|
|
394
|
+
const issuanceMarker = builders.issuanceMint(ISSUANCE_SPLICE_MARKER, paramsPolicy);
|
|
395
|
+
const markerBody = scriptBodyHex(issuanceMarker.compiledCode);
|
|
396
|
+
const splitParts = markerBody.split(ISSUANCE_SPLICE_MARKER);
|
|
397
|
+
/**
|
|
398
|
+
* ⛔ A HARD FAILURE, AND IT MUST NOT BECOME A WARNING. The splice reassembles
|
|
399
|
+
* the script from `pre + <real minting logic hash> + post`, so it is correct
|
|
400
|
+
* only while the marker occurs EXACTLY ONCE. Zero means the parameter is no
|
|
401
|
+
* longer inlined where we think; two means the splice would silently rewrite
|
|
402
|
+
* an unrelated byte run.
|
|
403
|
+
*
|
|
404
|
+
* ⚠ And flat UPLC is BIT-packed: a parameter is findable as a whole number of
|
|
405
|
+
* hex bytes only when it happens to land on a byte boundary. That alignment
|
|
406
|
+
* is a property to be MEASURED per artefact, never assumed — this check is
|
|
407
|
+
* what measures it.
|
|
408
|
+
*/
|
|
409
|
+
if (splitParts.length !== 2) {
|
|
410
|
+
throw new Error(`planBootstrap: the issuance_mint splice marker appeared ${splitParts.length - 1} times ` +
|
|
411
|
+
`in the compiled body — expected exactly once. The stored IssuanceCborHex datum is ` +
|
|
412
|
+
`reassembled around this cut, so zero occurrences means the parameter is no longer ` +
|
|
413
|
+
`inlined where this code expects, and more than one means the splice would rewrite an ` +
|
|
414
|
+
`unrelated byte run. Blueprint: "${blueprint.preamble.title}" v${blueprint.preamble.version}.`);
|
|
415
|
+
}
|
|
416
|
+
const [cborPre, cborPost] = splitParts;
|
|
417
|
+
const scripts = {
|
|
418
|
+
alwaysFail,
|
|
419
|
+
protocolParams,
|
|
420
|
+
programmableLogicBase,
|
|
421
|
+
issuanceCborHexMint,
|
|
422
|
+
registry,
|
|
423
|
+
transfer,
|
|
424
|
+
thirdParty,
|
|
425
|
+
unfracking,
|
|
426
|
+
programmableLogicGlobal,
|
|
427
|
+
issuanceLogic,
|
|
428
|
+
upgradeMultisig,
|
|
429
|
+
};
|
|
430
|
+
/**
|
|
431
|
+
* SIX fields, written BY NAME and never as a positional spread.
|
|
432
|
+
*
|
|
433
|
+
* ⛔ INDEX 1 CHANGED MEANING BETWEEN alpha.3 AND alpha.4. `issuance_logic_cred`
|
|
434
|
+
* was INSERTED at index 1, displacing `transfer_cred` to index 2. Both are
|
|
435
|
+
* `Credential` — same constructor, same 28 bytes — so a datum written in
|
|
436
|
+
* alpha.3's order with two fields appended is SIX fields long, passes the
|
|
437
|
+
* arity check, passes `params_wellformed` (which only asserts each credential
|
|
438
|
+
* is 28 bytes), decodes into a well-formed record, and hands `issuance_mint`
|
|
439
|
+
* the TRANSFER credential as its issuance authority. Nothing at deploy time
|
|
440
|
+
* catches it. Keying the record is what makes the compiler an ally.
|
|
441
|
+
*
|
|
442
|
+
* ⛔ `pendingUpgradeCred` MUST BE null AT GENESIS. `protocol_params` runs
|
|
443
|
+
* `params_wellformed(genesis_params, is_init: True)`, and `is_init: True` is
|
|
444
|
+
* exactly what forbids a nomination baked into the genesis datum.
|
|
445
|
+
*/
|
|
446
|
+
const paramsDatum = protocolParamsDatum({
|
|
447
|
+
plgCred: { type: "script", hash: programmableLogicGlobal.hash },
|
|
448
|
+
issuanceLogicCred: { type: "script", hash: issuanceLogic.hash },
|
|
449
|
+
transferCred: { type: "script", hash: transfer.hash },
|
|
450
|
+
thirdPartyCred: { type: "script", hash: thirdParty.hash },
|
|
451
|
+
upgradeCred: { type: "script", hash: upgradeMultisig.hash },
|
|
452
|
+
pendingUpgradeCred: null,
|
|
453
|
+
});
|
|
454
|
+
// Sentinel head of the registry linked list: key "", next 0xff*30, every
|
|
455
|
+
// delegate slot empty.
|
|
456
|
+
const EMPTY_CRED = { type: "key", hash: "" };
|
|
457
|
+
const originDatum = registryNodeDatum({
|
|
458
|
+
key: "",
|
|
459
|
+
next: "ff".repeat(30),
|
|
460
|
+
mintingLogicScript: EMPTY_CRED,
|
|
461
|
+
transferLogicScript: EMPTY_CRED,
|
|
462
|
+
thirdPartyTransferLogicScript: EMPTY_CRED,
|
|
463
|
+
unfrackingLogicScript: EMPTY_CRED,
|
|
464
|
+
globalStateCs: "",
|
|
465
|
+
});
|
|
466
|
+
const issuanceDatum = Data.constr(0n, [Data.bytearray(cborPre), Data.bytearray(cborPost)]);
|
|
467
|
+
const byName = {
|
|
468
|
+
programmableLogicBase,
|
|
469
|
+
programmableLogicGlobal,
|
|
470
|
+
transfer,
|
|
471
|
+
thirdParty,
|
|
472
|
+
unfracking,
|
|
473
|
+
issuanceLogic,
|
|
474
|
+
upgradeMultisig,
|
|
475
|
+
};
|
|
476
|
+
return {
|
|
477
|
+
config: { ...config, seeds, alwaysFailNonce },
|
|
478
|
+
scripts,
|
|
479
|
+
unfrackingParameter,
|
|
480
|
+
addresses: {
|
|
481
|
+
protocolParams: scriptAddress(config.networkId, paramsPolicy),
|
|
482
|
+
registry: scriptAddress(config.networkId, registry.hash),
|
|
483
|
+
issuanceCborHex: scriptAddress(config.networkId, alwaysFail.hash),
|
|
484
|
+
upgradeMultisig: scriptAddress(config.networkId, upgradeMultisig.hash),
|
|
485
|
+
},
|
|
486
|
+
assetUnits: {
|
|
487
|
+
protocolParamsNft: paramsPolicy + stringToHex("ProtocolParams"),
|
|
488
|
+
// Empty asset name, so the unit IS the policy.
|
|
489
|
+
registryNode: registry.hash,
|
|
490
|
+
issuanceCborHexNft: issuanceCborHexMint.hash + stringToHex("IssuanceCborHex"),
|
|
491
|
+
upgradeMultisigNft: upgradeMultisig.hash + stringToHex("UpgradeMultisig"),
|
|
492
|
+
},
|
|
493
|
+
datums: {
|
|
494
|
+
protocolParams: paramsDatum,
|
|
495
|
+
registryOrigin: originDatum,
|
|
496
|
+
issuanceCborHex: issuanceDatum,
|
|
497
|
+
},
|
|
498
|
+
issuanceCbor: { pre: cborPre, post: cborPost },
|
|
499
|
+
referenceScripts: REFERENCE_SCRIPT_ORDER.map((n) => byName[n]),
|
|
500
|
+
stakeCredentialScripts: STAKE_REGISTRATION_ORDER.map((n) => byName[n]),
|
|
501
|
+
parameterizations: events
|
|
502
|
+
.filter((e) => e.title !== STANDARD_VALIDATORS.ISSUANCE_MINT)
|
|
503
|
+
.map((e) => ({ rawScriptHash: e.rawScriptHash, params: e.params })),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const isSignBuilder = (built) => "chainResult" in built;
|
|
507
|
+
function requireBuildContext(ctx, step) {
|
|
508
|
+
if (!ctx || typeof ctx !== "object") {
|
|
509
|
+
throw new Error(`bootstrap ${step}: a build context is required.`);
|
|
510
|
+
}
|
|
511
|
+
if (!ctx.client || typeof ctx.client.newTx !== "function") {
|
|
512
|
+
throw new Error(`bootstrap ${step}: client is required — an Evolution SDK ReadOnlyClient or SigningClient. ` +
|
|
513
|
+
`This SDK does not construct clients, hold keys, or name endpoints.`);
|
|
514
|
+
}
|
|
515
|
+
if (typeof ctx.changeAddress !== "string" || ctx.changeAddress.length === 0) {
|
|
516
|
+
throw new Error(`bootstrap ${step}: changeAddress is required (bech32).`);
|
|
517
|
+
}
|
|
518
|
+
if (!Array.isArray(ctx.availableUtxos)) {
|
|
519
|
+
throw new Error(`bootstrap ${step}: availableUtxos is required — exactly the UTxOs this transaction may ` +
|
|
520
|
+
`spend. There is deliberately no default: without it coin selection is free to spend ` +
|
|
521
|
+
`reference-script UTxOs and seed UTxOs a later step still needs.`);
|
|
522
|
+
}
|
|
523
|
+
if (ctx.availableUtxos.length === 0) {
|
|
524
|
+
throw new Error(`bootstrap ${step}: availableUtxos is empty. Nothing can fund this transaction. If you ` +
|
|
525
|
+
`filtered a wallet view, check the filter before checking the wallet.`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function buildOptions(ctx) {
|
|
529
|
+
return {
|
|
530
|
+
changeAddress: EvoAddress.fromBech32(ctx.changeAddress),
|
|
531
|
+
availableUtxos: ctx.availableUtxos,
|
|
532
|
+
...(ctx.evaluator ? { evaluator: ctx.evaluator } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Build and shape into the SDK's one result type.
|
|
537
|
+
*
|
|
538
|
+
* ⚠ `_signBuilder` is the SAME field every other operation in this package
|
|
539
|
+
* returns, not a new escape hatch: `UnsignedTx` already carries it and the
|
|
540
|
+
* README's quick start already signs through it. Omitting it here would make
|
|
541
|
+
* the bootstrap the only operation whose result cannot be submitted — the
|
|
542
|
+
* defect `dummy.transfer` already recorded once. `cbor` is the supported path
|
|
543
|
+
* and the one a key-holding caller should prefer; this SDK signs nothing.
|
|
544
|
+
*/
|
|
545
|
+
async function finish(tx, ctx, step, metadata) {
|
|
546
|
+
const built = await tx.build(buildOptions(ctx));
|
|
547
|
+
const cbor = EvoTransaction.toCBORHex(await built.toTransaction());
|
|
548
|
+
const txHash = isSignBuilder(built) ? built.chainResult().txHash : "";
|
|
549
|
+
return { cbor, txHash, metadata: { step, ...metadata }, _signBuilder: built };
|
|
550
|
+
}
|
|
551
|
+
function requireSeedMatches(utxo, expected, role) {
|
|
552
|
+
if (!utxo || typeof utxo !== "object" || utxo.transactionId === undefined) {
|
|
553
|
+
throw new Error(`bootstrap: the ${role} seed UTxO is required — the unspent output this plan's one-shot ` +
|
|
554
|
+
`policy is parameterised by (${refKey(expected)}).`);
|
|
555
|
+
}
|
|
556
|
+
const actual = {
|
|
557
|
+
txHash: EvoTransactionHash.toHex(utxo.transactionId),
|
|
558
|
+
outputIndex: Number(utxo.index),
|
|
559
|
+
};
|
|
560
|
+
if (refKey(actual) !== refKey(expected)) {
|
|
561
|
+
throw new Error(`bootstrap: the ${role} seed UTxO is ${refKey(actual)}, but this plan was parameterised ` +
|
|
562
|
+
`by ${refKey(expected)}. A one-shot policy's hash is a function of its outref, so ` +
|
|
563
|
+
`spending a different UTxO builds a transaction against a script this deployment does ` +
|
|
564
|
+
`not own — and it fails on chain naming neither. Rebuild the plan from the seeds you ` +
|
|
565
|
+
`actually hold, or pass the seeds this plan names.`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Step 1 — fragment the wallet into {@link BOOTSTRAP_SEED_COUNT} distinct UTxOs.
|
|
570
|
+
*
|
|
571
|
+
* ⚑ THIS STEP IS OPTIONAL. A caller that already holds three distinct unspent
|
|
572
|
+
* UTxOs may skip it and call {@link planBootstrap} with their outrefs directly.
|
|
573
|
+
* It exists because most callers do not.
|
|
574
|
+
*
|
|
575
|
+
* ⚠ Its outputs cannot be predicted: the plan needs the OBSERVED outrefs. Submit
|
|
576
|
+
* this, wait for it, read the outputs back, then {@link selectBootstrapSeeds}.
|
|
577
|
+
*/
|
|
578
|
+
export async function buildSeedTx(params) {
|
|
579
|
+
requireBuildContext(params, "seed");
|
|
580
|
+
if (typeof params.ownerAddress !== "string" || params.ownerAddress.length === 0) {
|
|
581
|
+
throw new Error("bootstrap seed: ownerAddress is required (bech32).");
|
|
582
|
+
}
|
|
583
|
+
if (typeof params.seedLovelace !== "bigint" || params.seedLovelace <= 0n) {
|
|
584
|
+
throw new Error(`bootstrap seed: seedLovelace is required and must be a positive bigint, got ` +
|
|
585
|
+
`${JSON.stringify(String(params.seedLovelace))}. Each seed funds the transaction that ` +
|
|
586
|
+
`consumes it, so it must clear min-UTxO with room for a fee — a figure this package ` +
|
|
587
|
+
`cannot choose for a chain it does not know.`);
|
|
588
|
+
}
|
|
589
|
+
// ⛔ REQUIRED IS NOT THE SAME AS VALID (F-11). A caller passing `1n` built
|
|
590
|
+
// happily and failed only at submission, as "insufficient Ada" — the exact
|
|
591
|
+
// failure min-UTxO solving exists to prevent, arriving from the one figure
|
|
592
|
+
// this builder does NOT solve because the caller chose it.
|
|
593
|
+
const { coinsPerUtxoByte } = await params.client.getProtocolParameters();
|
|
594
|
+
const seedFloor = minLovelaceForPlainOutput(params.ownerAddress, coinsPerUtxoByte);
|
|
595
|
+
if (params.seedLovelace < seedFloor) {
|
|
596
|
+
throw new Error(`bootstrap seed: seedLovelace is ${params.seedLovelace} lovelace, below the min-UTxO ` +
|
|
597
|
+
`floor of ${seedFloor} for a plain output at ${params.ownerAddress} ` +
|
|
598
|
+
`(coinsPerUtxoByte ${coinsPerUtxoByte}). The ledger would reject the seed transaction ` +
|
|
599
|
+
`as "insufficient Ada" at submission. ⚠ This floor is NECESSARY, NOT SUFFICIENT: each ` +
|
|
600
|
+
`seed also has to fund the fee of the transaction that consumes it, and nothing here ` +
|
|
601
|
+
`checks that.`);
|
|
602
|
+
}
|
|
603
|
+
const owner = EvoAddress.fromBech32(params.ownerAddress);
|
|
604
|
+
let tx = params.client.newTx();
|
|
605
|
+
for (let i = 0; i < BOOTSTRAP_SEED_COUNT; i++) {
|
|
606
|
+
tx = tx.payToAddress({ address: owner, assets: EvoAssets.fromLovelace(params.seedLovelace) });
|
|
607
|
+
}
|
|
608
|
+
return finish(tx, params, "seed", { seedCount: BOOTSTRAP_SEED_COUNT });
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Pick the three seeds out of a seed transaction's observed outputs.
|
|
612
|
+
*
|
|
613
|
+
* ⚠ REFUSES FEWER THAN THREE, LOUDLY. An earlier version of this logic asked
|
|
614
|
+
* for three and tolerated two, which handed `undefined` to the third consumer.
|
|
615
|
+
*
|
|
616
|
+
* The change output of the seed transaction lands at the same address and is a
|
|
617
|
+
* legitimate candidate; ordering by output index keeps the assignment stable
|
|
618
|
+
* across reads of an indexer that does not preserve order.
|
|
619
|
+
*/
|
|
620
|
+
export function selectBootstrapSeeds(utxos, seedTxHash) {
|
|
621
|
+
const wanted = requireHex(seedTxHash, "seedTxHash", 32).toLowerCase();
|
|
622
|
+
const mine = utxos
|
|
623
|
+
.filter((u) => EvoTransactionHash.toHex(u.transactionId).toLowerCase() === wanted)
|
|
624
|
+
.sort((a, b) => Number(a.index) - Number(b.index));
|
|
625
|
+
if (mine.length < BOOTSTRAP_SEED_COUNT) {
|
|
626
|
+
throw new Error(`bootstrap: the seed transaction ${wanted} shows ${mine.length} output(s) in the UTxO set ` +
|
|
627
|
+
`supplied, need ≥${BOOTSTRAP_SEED_COUNT}. If the transaction was submitted, the ` +
|
|
628
|
+
`provider's view has probably not settled yet — wait for it rather than proceeding ` +
|
|
629
|
+
`with fewer, which hands a later step an undefined seed.`);
|
|
630
|
+
}
|
|
631
|
+
const [one, two, three] = mine;
|
|
632
|
+
const ref = (u) => ({
|
|
633
|
+
txHash: EvoTransactionHash.toHex(u.transactionId).toLowerCase(),
|
|
634
|
+
outputIndex: Number(u.index),
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
seeds: {
|
|
638
|
+
protocolParams: ref(one),
|
|
639
|
+
issuance: ref(two),
|
|
640
|
+
upgradeMultisig: ref(three),
|
|
641
|
+
},
|
|
642
|
+
utxos: { protocolParams: one, issuance: two, upgradeMultisig: three },
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Step 2 — mint the `upgrade_multisig` one-shot NFT and lock it with the signer
|
|
647
|
+
* tree, BEFORE the protocol genesis names this authority.
|
|
648
|
+
*
|
|
649
|
+
* ⛔ THE ORDER IS THE POINT, AND IT IS NOT A STYLE CHOICE. Step 3 writes a
|
|
650
|
+
* genesis datum naming `upgrade_cred = Script(upgrade_multisig)`. That authority
|
|
651
|
+
* is usable only while its config UTxO exists — the tree lives there, not in the
|
|
652
|
+
* script's parameters. Running this AFTER the protocol genesis and failing
|
|
653
|
+
* leaves a protocol on chain naming an authority whose config UTxO does not
|
|
654
|
+
* exist: upstream's documented ONE-WAY BRICK, manufactured by transaction
|
|
655
|
+
* ordering rather than by any defect in the validators. Failing BEFORE the
|
|
656
|
+
* irreversible step costs one transaction and nothing else.
|
|
657
|
+
*
|
|
658
|
+
* The four rails `upgrade_multisig.mint` enforces, all satisfied here:
|
|
659
|
+
* 1. the named UTxO is consumed -> collectFrom
|
|
660
|
+
* 2. exactly one "UpgradeMultisig" token of this policy -> mintAssets
|
|
661
|
+
* 3. an output found by `has_nft_strict` -> the output below
|
|
662
|
+
* 4. well_formed(tree), NO reference script, address == from_script(policy)
|
|
663
|
+
*
|
|
664
|
+
* ⛔ THE NFT AND NOTHING ELSE in that output. `has_nft_strict` is strict about
|
|
665
|
+
* the WHOLE value: bundling any other asset with the config NFT means the output
|
|
666
|
+
* is simply NOT FOUND by `list.expect_find`, and the genesis fails naming
|
|
667
|
+
* nothing about bundling. Change is a separate output.
|
|
668
|
+
*
|
|
669
|
+
* ⚠ NO `script:` ON THE OUTPUT — rail 4 requires `reference_script == None`.
|
|
670
|
+
* The reference script is published in step 4 like every other one.
|
|
671
|
+
*
|
|
672
|
+
* ⚑ Additional outputs are the CALLER's to add in their own transaction if they
|
|
673
|
+
* want them. Rail 3 uses `list.expect_find`, which SKIPS a non-matching output
|
|
674
|
+
* rather than rejecting it, so a junk UTxO parked at this address is harmless —
|
|
675
|
+
* but it is not protocol state and this step will not mint one.
|
|
676
|
+
*/
|
|
677
|
+
export async function buildMultisigGenesisTx(params) {
|
|
678
|
+
requireBuildContext(params, "multisig-genesis");
|
|
679
|
+
const { plan } = params;
|
|
680
|
+
if (!plan || typeof plan !== "object" || !plan.scripts) {
|
|
681
|
+
throw new Error("bootstrap multisig-genesis: plan is required — see planBootstrap().");
|
|
682
|
+
}
|
|
683
|
+
if (!params.upgradeMultisigTree || typeof params.upgradeMultisigTree !== "object") {
|
|
684
|
+
throw new Error(`bootstrap multisig-genesis: upgradeMultisigTree is required — the MultisigScript tree ` +
|
|
685
|
+
`that IS this protocol's upgrade authority. There is no default: shipping one would ` +
|
|
686
|
+
`mean shipping a decision about who controls every deployment made with this package.`);
|
|
687
|
+
}
|
|
688
|
+
requireSeedMatches(params.seedUtxo, plan.config.seeds.upgradeMultisig, "upgradeMultisig");
|
|
689
|
+
// Enforces upstream's `well_formed`; throws by name on an unsatisfiable leaf.
|
|
690
|
+
const datum = multisigScriptDatum(params.upgradeMultisigTree);
|
|
691
|
+
const nftUnit = plan.assetUnits.upgradeMultisigNft;
|
|
692
|
+
const assets = new Map([[nftUnit, 1n]]);
|
|
693
|
+
const coinsPerUtxoByte = (await params.client.getProtocolParameters()).coinsPerUtxoByte;
|
|
694
|
+
const lovelace = genesisOutputLovelace({
|
|
695
|
+
address: plan.addresses.upgradeMultisig,
|
|
696
|
+
assets: outputAssets(0n, assets),
|
|
697
|
+
datum,
|
|
698
|
+
coinsPerUtxoByte,
|
|
699
|
+
});
|
|
700
|
+
let tx = params.client.newTx();
|
|
701
|
+
tx = tx.collectFrom({ inputs: [params.seedUtxo] });
|
|
702
|
+
tx = tx.mintAssets({ assets: mintAssetsFromMap(assets), redeemer: voidData() });
|
|
703
|
+
tx = tx.payToAddress({
|
|
704
|
+
address: EvoAddress.fromBech32(plan.addresses.upgradeMultisig),
|
|
705
|
+
assets: outputAssets(lovelace, assets),
|
|
706
|
+
datum: new InlineDatum.InlineDatum({ data: datum }),
|
|
707
|
+
});
|
|
708
|
+
tx = tx.attachScript({ script: buildEvoScript(plan.scripts.upgradeMultisig.compiledCode) });
|
|
709
|
+
return finish(tx, params, "multisig-genesis", {
|
|
710
|
+
configUtxoOutputIndex: 0,
|
|
711
|
+
nftUnit,
|
|
712
|
+
address: plan.addresses.upgradeMultisig,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Assert the multisig config UTxO on chain is the one the genesis datum is
|
|
717
|
+
* about to name. PURE — the caller fetches the UTxOs, this decides.
|
|
718
|
+
*
|
|
719
|
+
* ⛔ AN OPERABILITY GATE, NOT DECORATION. The measured instance of "correct and
|
|
720
|
+
* useless" in this repo is a deployment naming an authority credential that
|
|
721
|
+
* could not be registered at all: every hash reproduced, every read-back
|
|
722
|
+
* matched, every test passed, and the protocol's upgrade path was permanently
|
|
723
|
+
* unsatisfiable. "It exists and is well-formed" left "and can be used" untested.
|
|
724
|
+
* Run this between step 2 and step 3, so it is structurally impossible for the
|
|
725
|
+
* genesis datum to name an authority that is not there.
|
|
726
|
+
*
|
|
727
|
+
* ⚑ FILTERS STRUCTURALLY, BY POLICY, EXACTLY AS THE VALIDATOR DOES — not by
|
|
728
|
+
* equality against a unit string we ourselves built. A lookup keyed on our own
|
|
729
|
+
* constructed unit shares a blind spot with the code that constructed it: get
|
|
730
|
+
* the asset name wrong in both places and the check agrees with itself.
|
|
731
|
+
*/
|
|
732
|
+
export function assertMultisigConfigUtxo(params) {
|
|
733
|
+
const { plan } = params;
|
|
734
|
+
const policy = plan.scripts.upgradeMultisig.hash;
|
|
735
|
+
const address = plan.addresses.upgradeMultisig;
|
|
736
|
+
const candidates = (params.utxosAtAddress ?? []).filter((u) => EvoAssets.getUnits(u.assets).some((unit) => unit !== "lovelace" && unit.slice(0, 56) === policy));
|
|
737
|
+
if (candidates.length !== 1) {
|
|
738
|
+
throw new Error(`upgrade_multisig config UTxO: expected exactly 1 UTxO at ${address} carrying an asset ` +
|
|
739
|
+
`of policy ${policy}, found ${candidates.length}. The NFT is one-shot, so zero means ` +
|
|
740
|
+
`the genesis output is not where the validator locks it, and more than one means this ` +
|
|
741
|
+
`address is not what we think it is. Refusing to let a genesis datum name an authority ` +
|
|
742
|
+
`whose config UTxO is not exactly one well-formed UTxO.`);
|
|
743
|
+
}
|
|
744
|
+
const utxo = candidates[0];
|
|
745
|
+
const onChain = getInlineDatum(utxo);
|
|
746
|
+
if (!onChain) {
|
|
747
|
+
throw new Error(`upgrade_multisig config UTxO at ${address} carries no inline datum. The tree IS the ` +
|
|
748
|
+
`authority; without it the credential is unsatisfiable and there is no repair path.`);
|
|
749
|
+
}
|
|
750
|
+
// ⚠ `decodeMultisigScript` deliberately enforces none of upstream's
|
|
751
|
+
// `well_formed` rules — that asymmetry with the encoder is intentional — so
|
|
752
|
+
// the SHAPE is asserted here, against what the caller said it minted.
|
|
753
|
+
const tree = decodeMultisigScript(onChain);
|
|
754
|
+
if (!sameTree(tree, params.expectedTree)) {
|
|
755
|
+
throw new Error(`upgrade_multisig config UTxO holds a different authority tree than the one this ` +
|
|
756
|
+
`bootstrap minted.\n expected: ${JSON.stringify(params.expectedTree, replacer)}\n ` +
|
|
757
|
+
`on chain: ${JSON.stringify(tree, replacer)}\nAn authority nobody can satisfy is a ` +
|
|
758
|
+
`permanent brick with no repair path, so this refuses before the genesis rather than ` +
|
|
759
|
+
`after it.`);
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
utxo,
|
|
763
|
+
ref: {
|
|
764
|
+
txHash: EvoTransactionHash.toHex(utxo.transactionId).toLowerCase(),
|
|
765
|
+
outputIndex: Number(utxo.index),
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const replacer = (_k, v) => (typeof v === "bigint" ? `${v}` : v);
|
|
770
|
+
/** Structural equality for a MultisigScript tree. Order-sensitive, as the encoding is. */
|
|
771
|
+
function sameTree(a, b) {
|
|
772
|
+
if (a.type !== b.type)
|
|
773
|
+
return false;
|
|
774
|
+
switch (a.type) {
|
|
775
|
+
case "signature":
|
|
776
|
+
return a.keyHash.toLowerCase() === b.keyHash.toLowerCase();
|
|
777
|
+
case "script":
|
|
778
|
+
return a.scriptHash.toLowerCase() === b.scriptHash.toLowerCase();
|
|
779
|
+
case "before":
|
|
780
|
+
case "after":
|
|
781
|
+
return a.time === b.time;
|
|
782
|
+
case "at-least":
|
|
783
|
+
if (a.required !== b.required)
|
|
784
|
+
return false;
|
|
785
|
+
// falls through — the child list is compared the same way
|
|
786
|
+
case "all-of":
|
|
787
|
+
case "any-of": {
|
|
788
|
+
const bs = b.scripts;
|
|
789
|
+
const as = a.scripts;
|
|
790
|
+
return as.length === bs.length && as.every((c, i) => sameTree(c, bs[i]));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Step 3 — the protocol genesis: three one-shot mints and the three UTxOs that
|
|
796
|
+
* hold this instance's state.
|
|
797
|
+
*
|
|
798
|
+
* Outputs, and the indices are POSITIONAL — `assembleDeploymentParams` reads
|
|
799
|
+
* output 0 and nothing checks the rest but the chain:
|
|
800
|
+
* 0 the params UTxO (NFT + the six-field params datum)
|
|
801
|
+
* 1 the registry origin node (NFT + the sentinel head of the linked list)
|
|
802
|
+
* 2 the issuance CBOR UTxO (NFT + the spliced issuance_mint body)
|
|
803
|
+
*/
|
|
804
|
+
export async function buildProtocolGenesisTx(params) {
|
|
805
|
+
requireBuildContext(params, "protocol-genesis");
|
|
806
|
+
const { plan } = params;
|
|
807
|
+
if (!plan || typeof plan !== "object" || !plan.scripts) {
|
|
808
|
+
throw new Error("bootstrap protocol-genesis: plan is required — see planBootstrap().");
|
|
809
|
+
}
|
|
810
|
+
requireSeedMatches(params.protocolParamsSeedUtxo, plan.config.seeds.protocolParams, "protocolParams");
|
|
811
|
+
requireSeedMatches(params.issuanceSeedUtxo, plan.config.seeds.issuance, "issuance");
|
|
812
|
+
const coinsPerUtxoByte = (await params.client.getProtocolParameters()).coinsPerUtxoByte;
|
|
813
|
+
const paramsNft = new Map([[plan.assetUnits.protocolParamsNft, 1n]]);
|
|
814
|
+
const registryNft = new Map([[plan.assetUnits.registryNode, 1n]]);
|
|
815
|
+
const issuanceNft = new Map([[plan.assetUnits.issuanceCborHexNft, 1n]]);
|
|
816
|
+
let tx = params.client.newTx();
|
|
817
|
+
if (params.provenancePin) {
|
|
818
|
+
const record = buildCip171RecordFromPin(plan.config.blueprint, params.provenancePin, plan.parameterizations);
|
|
819
|
+
tx = tx.attachMetadata({
|
|
820
|
+
label: CIP171_METADATA_LABEL,
|
|
821
|
+
metadata: buildCip171Metadatum(record),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
tx = tx.collectFrom({ inputs: [params.protocolParamsSeedUtxo, params.issuanceSeedUtxo] });
|
|
825
|
+
// ⚠ THREE POLICIES, THREE REDEEMERS, EACH THE GENESIS ARM OF ITS OWN
|
|
826
|
+
// VALIDATOR. The indices are not a shared enum and do not correspond to each
|
|
827
|
+
// other; they are copied verbatim from the sequence proven on chain.
|
|
828
|
+
tx = tx.mintAssets({
|
|
829
|
+
assets: mintAssetsFromMap(registryNft),
|
|
830
|
+
redeemer: registryInitRedeemer(),
|
|
831
|
+
});
|
|
832
|
+
tx = tx.mintAssets({
|
|
833
|
+
assets: mintAssetsFromMap(paramsNft),
|
|
834
|
+
redeemer: Data.constr(1n, []),
|
|
835
|
+
});
|
|
836
|
+
tx = tx.mintAssets({
|
|
837
|
+
assets: mintAssetsFromMap(issuanceNft),
|
|
838
|
+
redeemer: Data.constr(2n, []),
|
|
839
|
+
});
|
|
840
|
+
// ⚠ SOLVED, NOT FLAT, for every datum-bearing output here. The params datum
|
|
841
|
+
// grew from four fields to six and the origin node from five to seven;
|
|
842
|
+
// min-UTxO scales with serialised output size, and an under-funded output is
|
|
843
|
+
// reported as "insufficient Ada" — which sends the reader to the wallet
|
|
844
|
+
// balance rather than to the datum.
|
|
845
|
+
tx = tx.payToAddress({
|
|
846
|
+
address: EvoAddress.fromBech32(plan.addresses.protocolParams),
|
|
847
|
+
assets: outputAssets(genesisOutputLovelace({
|
|
848
|
+
address: plan.addresses.protocolParams,
|
|
849
|
+
assets: outputAssets(0n, paramsNft),
|
|
850
|
+
datum: plan.datums.protocolParams,
|
|
851
|
+
coinsPerUtxoByte,
|
|
852
|
+
}), paramsNft),
|
|
853
|
+
datum: new InlineDatum.InlineDatum({ data: plan.datums.protocolParams }),
|
|
854
|
+
});
|
|
855
|
+
tx = tx.payToAddress({
|
|
856
|
+
address: EvoAddress.fromBech32(plan.addresses.registry),
|
|
857
|
+
assets: outputAssets(ceilToWholeAda(minUtxoAtLeast(REGISTRY_NODE_MIN_ADA, {
|
|
858
|
+
address: plan.addresses.registry,
|
|
859
|
+
assets: outputAssets(0n, registryNft),
|
|
860
|
+
datum: plan.datums.registryOrigin,
|
|
861
|
+
coinsPerUtxoByte,
|
|
862
|
+
})), registryNft),
|
|
863
|
+
datum: new InlineDatum.InlineDatum({ data: plan.datums.registryOrigin }),
|
|
864
|
+
});
|
|
865
|
+
// The issuance datum carries the whole spliced script body, so its min-UTxO
|
|
866
|
+
// is an order of magnitude above the others. Solved for the same reason.
|
|
867
|
+
tx = tx.payToAddress({
|
|
868
|
+
address: EvoAddress.fromBech32(plan.addresses.issuanceCborHex),
|
|
869
|
+
assets: outputAssets(genesisOutputLovelace({
|
|
870
|
+
address: plan.addresses.issuanceCborHex,
|
|
871
|
+
assets: outputAssets(0n, issuanceNft),
|
|
872
|
+
datum: plan.datums.issuanceCborHex,
|
|
873
|
+
coinsPerUtxoByte,
|
|
874
|
+
}), issuanceNft),
|
|
875
|
+
datum: new InlineDatum.InlineDatum({ data: plan.datums.issuanceCborHex }),
|
|
876
|
+
});
|
|
877
|
+
tx = tx.attachScript({ script: buildEvoScript(plan.scripts.registry.compiledCode) });
|
|
878
|
+
tx = tx.attachScript({ script: buildEvoScript(plan.scripts.protocolParams.compiledCode) });
|
|
879
|
+
tx = tx.attachScript({ script: buildEvoScript(plan.scripts.issuanceCborHexMint.compiledCode) });
|
|
880
|
+
return finish(tx, params, "protocol-genesis", {
|
|
881
|
+
outputIndices: { protocolParams: 0, registryOrigin: 1, issuanceCborHex: 2 },
|
|
882
|
+
cip171: Boolean(params.provenancePin),
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Step 4 — publish the seven reference scripts, in {@link REFERENCE_SCRIPT_ORDER}.
|
|
887
|
+
*
|
|
888
|
+
* Cannot be folded into step 3: a transaction cannot reference a script it is
|
|
889
|
+
* itself creating.
|
|
890
|
+
*/
|
|
891
|
+
export async function buildReferenceScriptsTx(params) {
|
|
892
|
+
requireBuildContext(params, "reference-scripts");
|
|
893
|
+
const { plan } = params;
|
|
894
|
+
if (!plan || typeof plan !== "object" || !plan.referenceScripts) {
|
|
895
|
+
throw new Error("bootstrap reference-scripts: plan is required — see planBootstrap().");
|
|
896
|
+
}
|
|
897
|
+
if (typeof params.referenceScriptAddress !== "string" ||
|
|
898
|
+
params.referenceScriptAddress.length === 0) {
|
|
899
|
+
throw new Error(`bootstrap reference-scripts: referenceScriptAddress is required (bech32). These outputs ` +
|
|
900
|
+
`are protocol infrastructure for the life of the deployment; where they live is a ` +
|
|
901
|
+
`decision, not a default.`);
|
|
902
|
+
}
|
|
903
|
+
if (typeof params.referenceScriptLovelace !== "bigint" || params.referenceScriptLovelace <= 0n) {
|
|
904
|
+
throw new Error(`bootstrap reference-scripts: referenceScriptLovelace is required and must be a positive ` +
|
|
905
|
+
`bigint, got ${JSON.stringify(String(params.referenceScriptLovelace))}. min-UTxO for a ` +
|
|
906
|
+
`script-bearing output scales with the script's size.`);
|
|
907
|
+
}
|
|
908
|
+
// ⛔ REQUIRED IS NOT THE SAME AS VALID (F-11), and a script-bearing output is
|
|
909
|
+
// where the gap bites hardest: the script's own bytes are part of the
|
|
910
|
+
// serialised output, so its min-UTxO is an order of magnitude above a plain
|
|
911
|
+
// one. A caller passing a plain-output figure builds happily and is refused
|
|
912
|
+
// at submission as "insufficient Ada".
|
|
913
|
+
//
|
|
914
|
+
// ⚠ A SOUND LOWER BOUND, NOT THE EXACT FIGURE. `minUtxoForOutput` takes no
|
|
915
|
+
// script, so this adds the largest script's own bytes at `coinsPerUtxoByte`
|
|
916
|
+
// each. The true requirement is higher by the script ref's CBOR wrapping —
|
|
917
|
+
// which is why this REFUSES BELOW the bound and never reports it as
|
|
918
|
+
// sufficient.
|
|
919
|
+
const { coinsPerUtxoByte, maxTxSize } = await params.client.getProtocolParameters();
|
|
920
|
+
let largest = { name: "(none)", bytes: 0 };
|
|
921
|
+
plan.referenceScripts.forEach((script, i) => {
|
|
922
|
+
const bytes = scriptBodyHex(script.compiledCode).length / 2;
|
|
923
|
+
if (bytes > largest.bytes)
|
|
924
|
+
largest = { name: REFERENCE_SCRIPT_ORDER[i], bytes };
|
|
925
|
+
});
|
|
926
|
+
const refFloor = minLovelaceForPlainOutput(params.referenceScriptAddress, coinsPerUtxoByte) +
|
|
927
|
+
coinsPerUtxoByte * BigInt(largest.bytes);
|
|
928
|
+
if (params.referenceScriptLovelace < refFloor) {
|
|
929
|
+
throw new Error(`bootstrap reference-scripts: referenceScriptLovelace is ` +
|
|
930
|
+
`${params.referenceScriptLovelace} lovelace, below the ${refFloor} a script-bearing ` +
|
|
931
|
+
`output needs at coinsPerUtxoByte ${coinsPerUtxoByte} — the largest script here is ` +
|
|
932
|
+
`${largest.name} at ${largest.bytes} bytes, and a script's own bytes count toward ` +
|
|
933
|
+
`min-UTxO. The ledger would reject this as "insufficient Ada" at submission. ⚠ The ` +
|
|
934
|
+
`figure quoted is a LOWER BOUND: the true requirement is higher by the script ref's ` +
|
|
935
|
+
`CBOR wrapping.`);
|
|
936
|
+
}
|
|
937
|
+
const to = EvoAddress.fromBech32(params.referenceScriptAddress);
|
|
938
|
+
let tx = params.client.newTx();
|
|
939
|
+
for (const script of plan.referenceScripts) {
|
|
940
|
+
tx = tx.payToAddress({
|
|
941
|
+
address: to,
|
|
942
|
+
assets: outputAssets(params.referenceScriptLovelace),
|
|
943
|
+
script: buildEvoScript(script.compiledCode),
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const unsigned = await finish(tx, params, "reference-scripts", {
|
|
947
|
+
order: [...REFERENCE_SCRIPT_ORDER],
|
|
948
|
+
outputIndices: Object.fromEntries(REFERENCE_SCRIPT_ORDER.map((n, i) => [n, i])),
|
|
949
|
+
});
|
|
950
|
+
// ⛔ THE SIZE CAP, CHECKED HERE RATHER THAN DISCOVERED AT SUBMISSION (F-9).
|
|
951
|
+
// This transaction carries every published script body at once and is the
|
|
952
|
+
// one that grows when REFERENCE_SCRIPT_ORDER grows — MEASURED at 12,496 of
|
|
953
|
+
// 16,384 bytes for the current seven. Unsigned, so this is a LOWER BOUND on
|
|
954
|
+
// the signed size; it refuses the certain failures and cannot promise the
|
|
955
|
+
// marginal ones.
|
|
956
|
+
const bytes = unsigned.cbor.length / 2;
|
|
957
|
+
if (bytes >= maxTxSize) {
|
|
958
|
+
throw new Error(`bootstrap reference-scripts: the unsigned transaction publishing ` +
|
|
959
|
+
`${plan.referenceScripts.length} reference scripts is ${bytes} bytes, at or over this ` +
|
|
960
|
+
`chain's ${maxTxSize}-byte maximum — and witnesses have not been added yet. Split the ` +
|
|
961
|
+
`publication across two transactions and record both hashes; do not shorten ` +
|
|
962
|
+
`REFERENCE_SCRIPT_ORDER, whose indices a deployment record already depends on.`);
|
|
963
|
+
}
|
|
964
|
+
return { ...unsigned, metadata: { ...unsigned.metadata, unsignedBytes: bytes, maxTxSize } };
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Step 5 — the six Conway `RegCert`s, in one transaction.
|
|
968
|
+
*
|
|
969
|
+
* ⛔ `registerStake` + `attachScript` + a void redeemer, NEVER
|
|
970
|
+
* `registerAndDelegateTo`, FOR A SCRIPT CREDENTIAL. MEASURED: a combined
|
|
971
|
+
* certificate is a Conway `vote_reg_deleg_cert`, which arrives at the `publish`
|
|
972
|
+
* handler as a DIFFERENT `Certificate` constructor, and both
|
|
973
|
+
* `upgrade_multisig.publish` and `issuance_logic.publish` admit
|
|
974
|
+
* `RegisterCredential` and nothing else.
|
|
975
|
+
*
|
|
976
|
+
* ⚠ The DRep delegation a KEY credential needs is not missing here by oversight.
|
|
977
|
+
* A script withdraw-0 needs three things — a script witness, a registration, and
|
|
978
|
+
* the withdrawal itself. The DRep delegation is the FOURTH thing a key
|
|
979
|
+
* credential needs, and a script cannot have one.
|
|
980
|
+
*
|
|
981
|
+
* ⚠ Kept separate from step 3 because each certificate executes its script under
|
|
982
|
+
* the PUBLISH purpose: carrying all six script bodies alongside the mint
|
|
983
|
+
* witnesses is what breaks the 16,384-byte size cap. MEASURED at 21,816 bytes
|
|
984
|
+
* when this was one transaction.
|
|
985
|
+
*/
|
|
986
|
+
export async function buildStakeRegistrationTx(params) {
|
|
987
|
+
requireBuildContext(params, "stake-registrations");
|
|
988
|
+
const { plan } = params;
|
|
989
|
+
if (!plan || typeof plan !== "object" || !plan.stakeCredentialScripts) {
|
|
990
|
+
throw new Error("bootstrap stake-registrations: plan is required — see planBootstrap().");
|
|
991
|
+
}
|
|
992
|
+
let tx = params.client.newTx();
|
|
993
|
+
for (const script of plan.stakeCredentialScripts) {
|
|
994
|
+
tx = tx.registerStake({
|
|
995
|
+
stakeCredential: Credential.makeScriptHash(Bytes.fromHex(script.hash)),
|
|
996
|
+
redeemer: voidData(),
|
|
997
|
+
});
|
|
998
|
+
tx = tx.attachScript({ script: buildEvoScript(script.compiledCode) });
|
|
999
|
+
}
|
|
1000
|
+
return finish(tx, params, "stake-registrations", {
|
|
1001
|
+
order: [...STAKE_REGISTRATION_ORDER],
|
|
1002
|
+
credentials: plan.stakeCredentialScripts.map((s) => s.hash),
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Assemble `DeploymentParams` from the plan and what was observed. Pure.
|
|
1007
|
+
*
|
|
1008
|
+
* ⛔ THE RECORD MUST SAY WHAT THE DATUM SAYS. `upgradeAuthority` is the
|
|
1009
|
+
* deployment's account of the ACTIVE authority and the genesis datum writes
|
|
1010
|
+
* `upgradeCred: Script(upgradeMultisig)`. Nothing derives one from the other and
|
|
1011
|
+
* nothing may check them against each other on chain — which is exactly why
|
|
1012
|
+
* both come from the same plan here, rather than being written twice.
|
|
1013
|
+
*/
|
|
1014
|
+
export function assembleDeploymentParams(plan, observed) {
|
|
1015
|
+
if (!plan || typeof plan !== "object" || !plan.scripts) {
|
|
1016
|
+
throw new Error("assembleDeploymentParams: plan is required — see planBootstrap().");
|
|
1017
|
+
}
|
|
1018
|
+
const genesisTxHash = requireHex(observed?.protocolGenesisTxHash, "observed.protocolGenesisTxHash", 32).toLowerCase();
|
|
1019
|
+
const refTxHash = requireHex(observed?.referenceScriptsTxHash, "observed.referenceScriptsTxHash", 32).toLowerCase();
|
|
1020
|
+
const multisigUtxo = requireTxInput(observed?.multisigConfigUtxo, "observed.multisigConfigUtxo");
|
|
1021
|
+
const refIdx = (name) => ({
|
|
1022
|
+
txHash: refTxHash,
|
|
1023
|
+
outputIndex: REFERENCE_SCRIPT_ORDER.indexOf(name),
|
|
1024
|
+
});
|
|
1025
|
+
const s = plan.scripts;
|
|
1026
|
+
return {
|
|
1027
|
+
txHash: genesisTxHash,
|
|
1028
|
+
protocolParams: {
|
|
1029
|
+
txInput: plan.config.seeds.protocolParams,
|
|
1030
|
+
// ONE value: policy id AND address payment credential.
|
|
1031
|
+
policyId: s.protocolParams.hash,
|
|
1032
|
+
utxo: { txHash: genesisTxHash, outputIndex: 0 },
|
|
1033
|
+
},
|
|
1034
|
+
programmableLogicBase: { scriptHash: s.programmableLogicBase.hash },
|
|
1035
|
+
transfer: { scriptHash: s.transfer.hash },
|
|
1036
|
+
thirdParty: { scriptHash: s.thirdParty.hash },
|
|
1037
|
+
unfracking: { scriptHash: s.unfracking.hash },
|
|
1038
|
+
programmableLogicGlobal: {
|
|
1039
|
+
scriptHash: s.programmableLogicGlobal.hash,
|
|
1040
|
+
// ⛔ WHAT THE DISPATCHER WAS COMPILED AGAINST, and it is NOT derivable
|
|
1041
|
+
// from `unfracking.scriptHash` above: a deployment may have unfracking
|
|
1042
|
+
// fully deployed, published and recorded while compiling this dispatcher
|
|
1043
|
+
// against the sentinel, so the arm can never be satisfied. Two fields,
|
|
1044
|
+
// two different facts, and neither may be defaulted from the other.
|
|
1045
|
+
unfrackingParameter: plan.unfrackingParameter,
|
|
1046
|
+
},
|
|
1047
|
+
maxInlineDatumBytes: Number(plan.config.maxInlineDatumBytes),
|
|
1048
|
+
issuanceLogic: { scriptHash: s.issuanceLogic.hash },
|
|
1049
|
+
upgradeMultisig: {
|
|
1050
|
+
scriptHash: s.upgradeMultisig.hash,
|
|
1051
|
+
// ⚠ The upgradeMultisig seed, and NOT the protocolParams one. Same type,
|
|
1052
|
+
// not interchangeable — see BOOTSTRAP_SEED_COUNT.
|
|
1053
|
+
txInput: plan.config.seeds.upgradeMultisig,
|
|
1054
|
+
utxo: multisigUtxo,
|
|
1055
|
+
},
|
|
1056
|
+
upgradeAuthority: { type: "script", hash: s.upgradeMultisig.hash },
|
|
1057
|
+
issuance: {
|
|
1058
|
+
txInput: plan.config.seeds.issuance,
|
|
1059
|
+
policyId: s.issuanceCborHexMint.hash,
|
|
1060
|
+
alwaysFailScriptHash: s.alwaysFail.hash,
|
|
1061
|
+
},
|
|
1062
|
+
registry: {
|
|
1063
|
+
txInput: plan.config.seeds.protocolParams,
|
|
1064
|
+
issuanceScriptHash: s.issuanceCborHexMint.hash,
|
|
1065
|
+
// ONE value again: node NFT policy AND node address payment credential.
|
|
1066
|
+
scriptHash: s.registry.hash,
|
|
1067
|
+
},
|
|
1068
|
+
programmableBaseRefInput: refIdx("programmableLogicBase"),
|
|
1069
|
+
programmableLogicGlobalRefInput: refIdx("programmableLogicGlobal"),
|
|
1070
|
+
transferRefInput: refIdx("transfer"),
|
|
1071
|
+
thirdPartyRefInput: refIdx("thirdParty"),
|
|
1072
|
+
unfrackingRefInput: refIdx("unfracking"),
|
|
1073
|
+
issuanceLogicRefInput: refIdx("issuanceLogic"),
|
|
1074
|
+
upgradeMultisigRefInput: refIdx("upgradeMultisig"),
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
//# sourceMappingURL=bootstrap.js.map
|