diff --git a/.gitignore b/.gitignore
index 81813b3..962df4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ dist/
data/
bin/
release/
+vendor/kascov-preflight/target/
config/compiler.json
config/kascov-preflight.local.json
*.log
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b397712..3217944 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
# Changelog
+## 0.2.7 — 2026-08-09
+
+- Raised Studio's conservative covenant-cell default and minimum to 0.5 KAS/TKAS, preventing storage-mass rejection when funding a small covenant from a large faucet or mining UTXO.
+- Made genesis deployment choose the lowest suitable UTXO, calculate the exact candidate mass, and return structured diagnostics before signing when no candidate is standard.
+- Fixed Toccata v1 covenant and P2PK co-spend inputs to commit only `computeBudget`; non-zero legacy `sigOpCount` fields are now excluded because current nodes reject that mixed encoding.
+- Moved lifecycle fee calculation after full redeem-program and argument assembly, consumes the pinned script engine's fee/mass report, and reserves the final CheckSig execution cost before collecting signatures.
+- Kept phrase-free TN10 inheritance renewal fail-closed: only the exact current same-covenant continuation is accepted, with a dynamic fee capped at 0.02 TKAS.
+- Added regression coverage for large-faucet-UTXO deployment, engine-driven fee adjustment, signed execution reserve, Toccata v1 input encoding, and the renewal fee safety cap.
+- Completed live TN10 end-to-end verification for wallet transfer, covenant genesis, mature multi-inheritor distribution, and owner-signed check-in continuation; all final transactions passed the bundled engine and Kascov before node acceptance.
+- Vendored the audited MIT-licensed Kascov preflight source snapshot and its lockfile after the upstream repository removed the pinned commit; clean macOS, Windows, and Linux builds no longer depend on that upstream source remaining online. The lockfile also applies minimal fixes for `RUSTSEC-2026-0204` and `RUSTSEC-2026-0220`.
+
+## 0.2.7 — 2026-08-09(中文)
+
+- Studio 的保守 Covenant Cell 默认值与最低值统一提高到 0.5 KAS/TKAS,避免从水龙头或挖矿大额 UTXO 拆出过小契约输出时被存储质量规则拒绝。
+- 部署构建器会优先选择金额最小的合适 UTXO、逐笔计算真实质量;若没有标准交易候选,会在签名前返回结构化诊断。
+- 修复 Toccata v1 契约输入与 P2PK 共同授权输入:只提交 `computeBudget`,不再混入当前节点会拒绝的旧版非零 `sigOpCount`。
+- 生命周期手续费改为在 redeem program 与全部参数完整装配后计算,读取固定脚本引擎的费用/质量报告,并在收集签名前预留真实 CheckSig 执行费用。
+- TN10 免短语继承签到继续保持失效关闭:只允许当前项目、当前 UTXO、同 covenant ID 的唯一延续输出,动态手续费上限为 0.02 TKAS。
+- 增加大额水龙头 UTXO 部署、引擎动态费率、签名执行预留、Toccata v1 字段编码及续期费用上限的回归测试。
+- 已在真实 TN10 完成钱包转账、契约创世、多继承人成熟分配、拥有者签名签到延续的端到端验证;最终交易均先通过内置引擎和 Kascov,再由节点接受。
+- 在上游仓库删除原固定提交后,将已审计的 MIT 许可 Kascov 预检源码快照与锁文件直接纳入仓库;macOS、Windows、Linux 的干净构建不再依赖该上游源码持续在线,并以最小升级修复 `RUSTSEC-2026-0204` 与 `RUSTSEC-2026-0220`。
+
## 0.2.6 — 2026-08-09
- Updated the default SHA-256-pinned official SilverScript compiler to `cb34aa5e6a598f9e461c4ad7014279ba89251d8d`; the `2a3961c` legacy profile remains available for reproducibility.
diff --git a/README.md b/README.md
index 2d17cf2..7445b98 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,8 @@ npm ci
首次运行需要在当前系统构建固定版本的两个原生工具。以下命令由 Node.js 驱动,可直接用于 macOS、Windows 和 Linux:
+Kascov 预检引擎使用仓库内置的 MIT 许可源码快照和 Cargo 锁文件构建,不依赖 Kascov 网站或其历史 Git 提交继续在线。
+
```bash
npm run setup:silverc
npm run setup:kascov-preflight
@@ -338,6 +340,8 @@ npm ci
Build the two pinned native tools for the current platform. These Node.js-driven commands run directly on macOS, Windows, and Linux:
+The Kascov preflight helper builds from the committed MIT-licensed source snapshot and Cargo lockfile, so it does not depend on the Kascov website or a historical upstream Git commit remaining online.
+
```bash
npm run setup:silverc
npm run setup:kascov-preflight
diff --git a/config/kascov-preflight.json b/config/kascov-preflight.json
index 88e46a4..089285b 100644
--- a/config/kascov-preflight.json
+++ b/config/kascov-preflight.json
@@ -2,7 +2,9 @@
"repository": "https://github.com/Knitser/kascov.git",
"upstreamCommit": "b64d6b4114df324f899783080371f26b619b19d0",
"rustyKaspaCommit": "98a4ccd8d200853787f227bd4536ac540cf34957",
- "sourceModule": "crates/kascov/src/preflight.rs",
+ "sourceMode": "vendored-mit-snapshot",
+ "sourceModule": "vendor/kascov-preflight/crates/studio-kascov-preflight/src/preflight.rs",
+ "license": "vendor/kascov-preflight/LICENSE",
"binary": "bin/kascov-preflight",
"purpose": "Offline transaction preflight using the same Kaspa script engine path as Kascov"
}
diff --git a/docs/releases/v0.2.7.md b/docs/releases/v0.2.7.md
new file mode 100644
index 0000000..f60697c
--- /dev/null
+++ b/docs/releases/v0.2.7.md
@@ -0,0 +1,45 @@
+## Kaspa SilverScript Studio v0.2.7
+
+This release fixes three transaction-construction defects found by exercising the packaged Studio against the live Kaspa TN10 network, not only against mocks.
+
+### Transaction correctness
+
+- Studio now applies a conservative 0.5 KAS/TKAS covenant-cell floor. This keeps a genesis split funded from a large faucet or mining UTXO within both the bundled wallet calculator and current node storage-mass limits; it is not presented as a consensus dust rule.
+- Genesis deployment evaluates eligible UTXOs from smallest to largest and records exact mass diagnostics before any wallet signature.
+- Toccata v1 covenant and P2PK co-spend inputs now use `computeBudget` with a zero legacy `sigOpCount`, matching the current node RPC and consensus encoding.
+- Lifecycle packages are fully assembled before fee calculation. Studio consumes the pinned local script-engine mass/fee report and adds a conservative reserve for each real CheckSig before asking anyone to sign.
+- Phrase-free TN10 inheritance check-in remains narrowly authorized and fail-closed, with a 0.02 TKAS maximum automatic fee.
+- The audited MIT-licensed Kascov preflight source snapshot and Cargo lockfile are now committed with Studio. Rebuilding the local engine no longer depends on the removed upstream commit remaining reachable; minimal transitive updates address `RUSTSEC-2026-0204` and `RUSTSEC-2026-0220`.
+
+### Live TN10 evidence
+
+- Wallet transfer: [`f3734e73…`](https://kascov.io/#/testnet-10/tx/f3734e73bd96c0a00437b15298dcc284004af57386786278905a00cd15de53d5)
+- 0.5 TKAS inheritance covenant genesis: [`94d0414e…`](https://kascov.io/#/testnet-10/tx/94d0414ecb2b4c119841bbec5b732e882b82b3713abfca563b32975086aaa656)
+- Mature 50/50 inheritance distribution: [`185bac65…`](https://kascov.io/#/testnet-10/tx/185bac653855e7b99f36c7ac0f4b97af4b911ed1a88531ec5a9fb1b6e5ca4fd2)
+- Owner-signed same-covenant check-in continuation: [`103e9047…`](https://kascov.io/#/testnet-10/tx/103e9047cdffec4dd41f3b4632b09898fa78547a6d9a54ae0cc3093692adfb28)
+
+The final inheritance distribution and renewal both passed the bundled pinned Kaspa script engine and Kascov preflight before node submission. Automated tests cover 43 Studio behaviors; npm reports zero known vulnerabilities. Mainnet remains disabled by default, and SilverScript remains experimental.
+
+---
+
+## 中文说明
+
+本版修复了三个只有把打包后的 Studio 接入真实 Kaspa TN10 网络后才暴露的交易构建问题,而不是只依赖模拟测试。
+
+### 交易正确性
+
+- Studio 现在采用保守的 0.5 KAS/TKAS Covenant Cell 下限,避免从水龙头或挖矿大额 UTXO 拆分时超过内置钱包计算器与当前节点的存储质量限制;这不是共识层 dust 规则。
+- 契约创世部署会从小到大评估可用 UTXO,并在请求钱包签名前记录精确质量诊断。
+- Toccata v1 契约输入和 P2PK 共同授权输入现在只使用 `computeBudget`,旧版 `sigOpCount` 固定为零,与当前节点 RPC 和共识编码一致。
+- 生命周期操作包会先完整装配 redeem program 和参数,再读取固定本地脚本引擎的质量/费用报告;每个真实 CheckSig 都会在签名前加入保守费用预留。
+- TN10 免短语继承签到仍严格限制在当前项目、当前 UTXO 和同 Covenant 延续,自动手续费上限为 0.02 TKAS。
+- 已审计的 MIT 许可 Kascov 预检源码快照和 Cargo 锁文件现已随 Studio 一同提交;重建本地引擎不再依赖已被上游删除的历史提交仍可访问,并以最小依赖升级修复 `RUSTSEC-2026-0204` 与 `RUSTSEC-2026-0220`。
+
+### 真实 TN10 证据
+
+- 钱包转账:[`f3734e73…`](https://kascov.io/#/testnet-10/tx/f3734e73bd96c0a00437b15298dcc284004af57386786278905a00cd15de53d5)
+- 0.5 TKAS 继承契约创世:[`94d0414e…`](https://kascov.io/#/testnet-10/tx/94d0414ecb2b4c119841bbec5b732e882b82b3713abfca563b32975086aaa656)
+- 成熟后的 50/50 继承分配:[`185bac65…`](https://kascov.io/#/testnet-10/tx/185bac653855e7b99f36c7ac0f4b97af4b911ed1a88531ec5a9fb1b6e5ca4fd2)
+- 拥有者签名、同 Covenant 延续的签到交易:[`103e9047…`](https://kascov.io/#/testnet-10/tx/103e9047cdffec4dd41f3b4632b09898fa78547a6d9a54ae0cc3093692adfb28)
+
+最终继承分配与签到续期在提交节点前都通过了内置固定 Kaspa 脚本引擎和 Kascov 预检。自动化测试覆盖 43 项 Studio 行为,npm 已知漏洞为零。主网继续默认关闭,SilverScript 仍处于实验阶段。
diff --git a/index.html b/index.html
index f3179db..7fb69ea 100644
--- a/index.html
+++ b/index.html
@@ -167,7 +167,7 @@
契约项目
DEPLOYMENT INTENT
为 Covenant 创建链上 UTXO 04 / SIGN
主网确认短语
diff --git a/package-lock.json b/package-lock.json
index 271b523..b47c888 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "kaspa-silverscript-studio",
- "version": "0.2.6",
+ "version": "0.2.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kaspa-silverscript-studio",
- "version": "0.2.6",
+ "version": "0.2.7",
"license": "MIT",
"dependencies": {
"@kluster/kaspa-wasm": "2.0.1",
diff --git a/package.json b/package.json
index 1ada578..f5777d5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "kaspa-silverscript-studio",
- "version": "0.2.6",
+ "version": "0.2.7",
"private": true,
"type": "module",
"description": "Local bilingual AI-assisted SilverScript contract studio for Kaspa",
diff --git a/scripts/build-kascov-preflight.mjs b/scripts/build-kascov-preflight.mjs
index d413e5f..4df6fd9 100644
--- a/scripts/build-kascov-preflight.mjs
+++ b/scripts/build-kascov-preflight.mjs
@@ -6,95 +6,29 @@ import { fileURLToPath } from "node:url";
import { cargoReleaseBinary, executableName, makeExecutable } from "./platform-binaries.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
-const repositoryUrl = process.env.KASCOV_REPOSITORY || "https://github.com/Knitser/kascov.git";
-const commit = process.env.KASCOV_COMMIT || "b64d6b4114df324f899783080371f26b619b19d0";
-const work = path.resolve(process.env.KASCOV_BUILD_DIR || path.join(root, ".build", `kascov-${commit}`));
+const workspace = path.join(root, "vendor", "kascov-preflight");
+const manifest = path.join(workspace, "Cargo.toml");
+const lock = path.join(workspace, "Cargo.lock");
const targetDirectory = path.resolve(process.env.CARGO_TARGET_DIR || path.join(root, ".build", "kascov-target"));
const output = path.join(root, "bin", executableName("kascov-preflight"));
-const packageName = "studio-kascov-preflight";
function run(command, args, options = {}) {
execFileSync(command, args, { stdio: "inherit", ...options });
}
-function packageIdentities(lock) {
- return lock
- .replace(/\r\n/g, "\n")
- .split(/\n(?=\[\[package\]\]\n)/)
- .filter((section) => section.startsWith("[[package]]\n"))
- .map((section) => {
- const field = (name) => section.match(new RegExp(`^${name} = "([^"]*)"$`, "m"))?.[1] || "";
- return JSON.stringify([field("name"), field("version"), field("source"), field("checksum")]);
- });
+if (!fs.existsSync(manifest) || !fs.existsSync(lock)) {
+ throw new Error("Vendored Kascov preflight workspace is incomplete");
}
-if (!fs.existsSync(path.join(work, ".git"))) {
- fs.rmSync(work, { recursive: true, force: true });
- fs.mkdirSync(path.dirname(work), { recursive: true });
- run("git", ["clone", "--filter=blob:none", "--no-checkout", repositoryUrl, work]);
-}
-
-run("git", ["-C", work, "fetch", "--depth", "1", "origin", commit]);
-run("git", ["-C", work, "checkout", "--detach", "--force", commit]);
-const lockFile = path.join(work, "Cargo.lock");
-const pinnedLock = fs.readFileSync(lockFile, "utf8");
-const crateDirectory = path.join(work, "crates", packageName);
-const sourceDirectory = path.join(crateDirectory, "src");
-fs.mkdirSync(sourceDirectory, { recursive: true });
-fs.copyFileSync(path.join(root, "native", "kascov-preflight-main.rs"), path.join(sourceDirectory, "main.rs"));
-const upstreamPreflightFile = path.join(work, "crates", "kascov", "src", "preflight.rs");
-const upstreamPreflight = fs.readFileSync(upstreamPreflightFile, "utf8");
-if (!upstreamPreflight.includes("use kascov_core::Network;")) throw new Error("Pinned Kascov preflight source has an unexpected Network import");
-fs.writeFileSync(path.join(sourceDirectory, "preflight.rs"), upstreamPreflight.replace("use kascov_core::Network;", "use crate::Network;"));
-fs.writeFileSync(path.join(crateDirectory, "Cargo.toml"), `[package]
-name = "${packageName}"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-
-[dependencies]
-kascov-decode = { workspace = true }
-kascov-sim = { workspace = true }
-kaspa-consensus-core = { workspace = true }
-serde = { workspace = true }
-serde_json = { workspace = true }
-hex = { workspace = true }
-
-[[bin]]
-name = "kascov-preflight"
-path = "src/main.rs"
-`);
-const workspaceManifestFile = path.join(work, "Cargo.toml");
-const workspaceManifest = fs.readFileSync(workspaceManifestFile, "utf8");
-if (!workspaceManifest.includes("members = [")) throw new Error("Pinned Kascov workspace manifest has an unexpected members declaration");
-fs.writeFileSync(workspaceManifestFile, workspaceManifest.replace("members = [", `members = ["crates/${packageName}", `));
-const buildArgs = [
- "build",
- "--manifest-path", workspaceManifestFile,
- "--release",
- "-p", packageName,
- "--bin", "kascov-preflight"
-];
-// Cargo must first register the injected local package in the upstream lockfile.
-// The existing lockfile supplies every external resolution. Target-specific
-// dependency sections can be rewritten on Windows, so compare immutable package
-// identities instead of formatting: every pinned name/version/source/checksum
-// tuple must remain present before repeating the build under --locked.
-run("cargo", buildArgs, { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } });
-const updatedLock = fs.readFileSync(lockFile, "utf8");
-const pinnedPackages = packageIdentities(pinnedLock);
-const updatedPackages = new Set(packageIdentities(updatedLock));
-for (const identity of pinnedPackages) {
- if (!updatedPackages.has(identity)) throw new Error(`Pinned Kascov dependency drifted while adding the local preflight package: ${identity}`);
-}
-if (![...updatedPackages].some((identity) => JSON.parse(identity)[0] === packageName)) {
- throw new Error("Cargo did not register the local Kascov preflight package");
-}
run("cargo", [
"build",
"--locked",
- ...buildArgs.slice(1)
+ "--manifest-path", manifest,
+ "--release",
+ "-p", "studio-kascov-preflight",
+ "--bin", "kascov-preflight"
], { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } });
+
fs.mkdirSync(path.dirname(output), { recursive: true });
fs.copyFileSync(cargoReleaseBinary(targetDirectory, "kascov-preflight"), output);
makeExecutable(output);
diff --git a/scripts/check-syntax.mjs b/scripts/check-syntax.mjs
index a26a415..d0be2cc 100644
--- a/scripts/check-syntax.mjs
+++ b/scripts/check-syntax.mjs
@@ -15,6 +15,10 @@ for (const directory of ["server", "src", "scripts"]) {
}
for (const file of files) execFileSync(process.execPath, ["--check", file], { stdio: "inherit" });
const preflightManifest = JSON.parse(fs.readFileSync(path.join(root, "config", "kascov-preflight.json"), "utf8"));
+if (preflightManifest.sourceMode !== "vendored-mit-snapshot") throw new Error("Local preflight must use the vendored source snapshot");
+for (const requiredPath of [preflightManifest.sourceModule, preflightManifest.license, "vendor/kascov-preflight/Cargo.lock"]) {
+ if (!requiredPath || !fs.existsSync(path.join(root, requiredPath))) throw new Error(`Vendored preflight source is incomplete: ${requiredPath || "missing manifest path"}`);
+}
const preflightLocalManifestFile = path.join(root, "config", "kascov-preflight.local.json");
if (!fs.existsSync(preflightLocalManifestFile)) throw new Error("Local preflight manifest is missing. Run npm run setup:kascov-preflight.");
const preflightLocalManifest = JSON.parse(fs.readFileSync(preflightLocalManifestFile, "utf8"));
diff --git a/server/atomic-covenant-builder.mjs b/server/atomic-covenant-builder.mjs
index 5c70992..af33a69 100644
--- a/server/atomic-covenant-builder.mjs
+++ b/server/atomic-covenant-builder.mjs
@@ -103,7 +103,9 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena
previousOutpoint: outpoint,
signatureScript: "",
sequence: BigInt(item.sequence || 0),
- sigOpCount: Number(item.sigOpCount || 0),
+ // Toccata v1 inputs use computeBudget; a non-zero legacy sig-op field is
+ // rejected by current nodes before script execution.
+ sigOpCount: 0,
computeBudget: Number(item.computeBudget || 120),
utxo: transactionUtxo(utxo)
};
@@ -139,7 +141,8 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena
gas: 0n,
payload: ""
});
- const sigOps = Math.max(1, transactionInputs.reduce((sum, input) => sum + Number(input.sigOpCount || 0), 0));
+ const covenantSignatures = metadata.reduce((sum, item) => sum + (item.arguments || []).filter((argument) => argument?.kind === "signature").length, 0);
+ const sigOps = Math.max(1, covenantSignatures + (p2pkAuthorization?.input ? 1 : 0));
if (!kaspa.updateTransactionMass(network.kaspaNetworkId, transaction, sigOps, true)) throw builderError("Atomic covenant transaction exceeds the current mass limit", "ATOMIC_MASS_LIMIT");
return {
version: 1,
diff --git a/server/kaspa-service.mjs b/server/kaspa-service.mjs
index 8b67ab7..05763f2 100644
--- a/server/kaspa-service.mjs
+++ b/server/kaspa-service.mjs
@@ -8,7 +8,10 @@ import { CovenantStateSource, covenantStateProvider } from "./covenant-state-sou
const require = createRequire(import.meta.url);
const kaspa = require("@kluster/kaspa-wasm");
const SOMPI = 100_000_000n;
-const MIN_DEPLOY_SOMPI = 5_000_000n;
+// A 0.5 KAS covenant cell stays below the bundled wallet calculator's
+// conservative 100,000-mass ceiling even when it is funded from a large
+// faucet/mining UTXO. This is a Studio safety floor, not a consensus dust rule.
+const MIN_DEPLOY_SOMPI = 50_000_000n;
const DEFAULT_FEE_RESERVE = 2_000_000n;
function networkOf(id) {
@@ -505,7 +508,13 @@ export async function buildDeployDraft(input, draftStore) {
const publicKey = String(input.publicKey || "").trim();
assertAddressAndPublicKey(address, publicKey, network);
const amount = kasToSompi(input.amountKas);
- if (amount < MIN_DEPLOY_SOMPI) throw new Error("Covenant deployment requires at least 0.05 KAS/TKAS for standard mass headroom");
+ if (amount < MIN_DEPLOY_SOMPI) {
+ throw Object.assign(new Error("Studio requires at least 0.5 KAS/TKAS for conservative covenant storage-mass headroom"), {
+ status: 400,
+ code: "COVENANT_AMOUNT_BELOW_STANDARD_MASS",
+ report: { minimumAmountSompi: MIN_DEPLOY_SOMPI.toString(), minimumAmountKas: sompiToKas(MIN_DEPLOY_SOMPI) }
+ });
+ }
if (network.id === "mainnet" && amount > kasToSompi(config.mainnetMaxDeployKas)) throw new Error(`Mainnet deployment exceeds the ${config.mainnetMaxDeployKas} KAS local cap`);
const programHex = assertProgramHex(input.artifact?.programHex);
if (Array.isArray(input.artifact?.deploymentBlockedReasons) && input.artifact.deploymentBlockedReasons.length) {
@@ -525,38 +534,60 @@ export async function buildDeployDraft(input, draftStore) {
}
const utxos = await fetchSpendableUtxos(network, address);
const required = amount + DEFAULT_FEE_RESERVE;
- const utxo = utxos.find((item) => item.amount >= required);
- if (!utxo) throw new Error(`No plain spendable UTXO contains at least ${Number(required) / Number(SOMPI)} ${network.symbol}`);
-
- const inputValue = {
- previousOutpoint: utxo.outpoint,
- signatureScript: "",
- sequence: 0n,
- sigOpCount: 0,
- computeBudget: 120,
- utxo: {
- address,
- outpoint: utxo.outpoint,
- amount: utxo.amount,
- scriptPublicKey: new kaspa.ScriptPublicKey(0, utxo.script),
- blockDaaScore: utxo.blockDaaScore,
- isCoinbase: false
+ const candidates = utxos.filter((item) => item.amount >= required).sort((a, b) => a.amount < b.amount ? -1 : a.amount > b.amount ? 1 : 0);
+ if (!candidates.length) throw new Error(`No plain spendable UTXO contains at least ${Number(required) / Number(SOMPI)} ${network.symbol}`);
+
+ let transaction = null;
+ let lowestMass = null;
+ for (const utxo of candidates) {
+ const inputValue = {
+ previousOutpoint: utxo.outpoint,
+ signatureScript: "",
+ sequence: 0n,
+ sigOpCount: 0,
+ computeBudget: 120,
+ utxo: {
+ address,
+ outpoint: utxo.outpoint,
+ amount: utxo.amount,
+ scriptPublicKey: new kaspa.ScriptPublicKey(0, utxo.script),
+ blockDaaScore: utxo.blockDaaScore,
+ isCoinbase: false
+ }
+ };
+ const outputs = [new kaspa.TransactionOutput(amount, kaspa.payToScriptHashScript(programHex))];
+ const change = utxo.amount - required;
+ if (change > 0n) outputs.push(new kaspa.TransactionOutput(change, kaspa.payToAddressScript(address)));
+ const candidate = new kaspa.Transaction({
+ version: 1,
+ inputs: [inputValue],
+ outputs,
+ lockTime: 0n,
+ subnetworkId: "0000000000000000000000000000000000000000",
+ gas: 0n,
+ payload: ""
+ });
+ candidate.populateGenesisCovenants([{ authorizingInput: 0, outputs: [0] }]);
+ const mass = BigInt(kaspa.calculateTransactionMass(network.kaspaNetworkId, candidate, 1, true));
+ if (lowestMass === null || mass < lowestMass) lowestMass = mass;
+ if (kaspa.updateTransactionMass(network.kaspaNetworkId, candidate, 1, true)) {
+ transaction = candidate;
+ break;
}
- };
- const outputs = [new kaspa.TransactionOutput(amount, kaspa.payToScriptHashScript(programHex))];
- const change = utxo.amount - required;
- if (change > 0n) outputs.push(new kaspa.TransactionOutput(change, kaspa.payToAddressScript(address)));
- const transaction = new kaspa.Transaction({
- version: 1,
- inputs: [inputValue],
- outputs,
- lockTime: 0n,
- subnetworkId: "0000000000000000000000000000000000000000",
- gas: 0n,
- payload: ""
- });
- transaction.populateGenesisCovenants([{ authorizingInput: 0, outputs: [0] }]);
- if (!kaspa.updateTransactionMass(network.kaspaNetworkId, transaction, 1, true)) throw new Error("Transaction exceeds the standard mass limit");
+ try { candidate.free?.(); } catch {}
+ }
+ if (!transaction) {
+ const maximumMass = BigInt(kaspa.maximumStandardTransactionMass());
+ throw Object.assign(new Error(`No available UTXO can fund this covenant within the standard mass limit (${lowestMass ?? "unknown"}/${maximumMass})`), {
+ status: 400,
+ code: "COVENANT_DEPLOYMENT_MASS_LIMIT",
+ report: {
+ calculatedMass: lowestMass?.toString() || "",
+ maximumStandardMass: maximumMass.toString(),
+ minimumAmountKas: sompiToKas(MIN_DEPLOY_SOMPI)
+ }
+ });
+ }
const unsignedTransactionSafeJson = transaction.serializeToSafeJSON();
const safe = JSON.parse(unsignedTransactionSafeJson);
const covenantId = String(safe.outputs?.[0]?.covenant?.covenantId || "");
diff --git a/server/local-operation-authorization.mjs b/server/local-operation-authorization.mjs
index 34ecded..2fcf479 100644
--- a/server/local-operation-authorization.mjs
+++ b/server/local-operation-authorization.mjs
@@ -6,6 +6,8 @@ function lifecycleStateError(message, code) {
return Object.assign(new Error(message), { status: 409, code });
}
+const MAX_PHRASE_FREE_RENEWAL_FEE = 2_000_000n;
+
export function assertLocalRenewalOpen(status) {
if (!status?.unspent) throw authorizationError("The local inheritance covenant is no longer unspent");
if (status.schedule?.mature) throw authorizationError("The inheritance covenant has expired and can no longer be renewed");
@@ -41,7 +43,11 @@ export function assertLocalRenewalPackage(project, inspected, status = null) {
if (review.outputCount !== 1 || review.outputs?.[0]?.covenantId !== review.covenantId) {
throw authorizationError("Renewal must contain exactly one same-covenant continuation");
}
- if (review.feeSompi !== "1000000") throw authorizationError("Local one-click renewal fee must be exactly 0.01 TKAS");
+ let renewalFee;
+ try { renewalFee = BigInt(review.feeSompi); } catch { throw authorizationError("Local one-click renewal fee is invalid"); }
+ if (renewalFee < 1000n || renewalFee > MAX_PHRASE_FREE_RENEWAL_FEE) {
+ throw authorizationError("Local one-click renewal fee must be from 0.00001 to 0.02 TKAS");
+ }
const activeTxid = String(project.deployment?.activeTxid || project.deployment?.txid || "").toLowerCase();
if (!activeTxid || String(review.inputOutpoint?.transactionId || "").toLowerCase() !== activeTxid) {
throw authorizationError("Renewal does not spend the current local project UTXO");
diff --git a/server/p2pk-cospend.mjs b/server/p2pk-cospend.mjs
index fbe3d58..a2f95be 100644
--- a/server/p2pk-cospend.mjs
+++ b/server/p2pk-cospend.mjs
@@ -61,7 +61,8 @@ export function createP2pkCoSpendAuthorization({ network: networkId = "tn10", ad
previousOutpoint: normalized.outpoint,
signatureScript: "",
sequence: 0n,
- sigOpCount: 1,
+ // Toccata v1 inputs must leave the legacy sig-op field at zero.
+ sigOpCount: 0,
computeBudget: 10,
utxo: {
address: walletAddress,
diff --git a/server/project-store.mjs b/server/project-store.mjs
index 100ba41..8d2da72 100644
--- a/server/project-store.mjs
+++ b/server/project-store.mjs
@@ -54,7 +54,7 @@ export class ProjectStore {
constructorArgs: Array.isArray(input.constructorArgs) ? input.constructorArgs : [],
compilerProfileId: String(input.compilerProfileId || "latest-cb34aa5"),
templateParameters: input.templateParameters && typeof input.templateParameters === "object" ? input.templateParameters : {},
- deployAmount: String(input.deployAmount || "0.05"),
+ deployAmount: String(input.deployAmount || "0.5"),
specification: input.specification || null,
transactionPlans: Array.isArray(input.transactionPlans) ? input.transactionPlans : [],
review: input.review || null,
diff --git a/server/template-operation-service.mjs b/server/template-operation-service.mjs
index 9a957c4..039b077 100644
--- a/server/template-operation-service.mjs
+++ b/server/template-operation-service.mjs
@@ -2,12 +2,17 @@ import crypto from "node:crypto";
import { createRequire } from "node:module";
import { NETWORKS } from "./config.mjs";
import { finalizeExternalCovenantPackage, inspectExternalCovenantPackage } from "./external-covenant-service.mjs";
-import { findCovenantUtxo, kasToSompi, kascovPreflight } from "./kaspa-service.mjs";
+import { findCovenantUtxo, kasToSompi, kascovPreflight, sompiToKas } from "./kaspa-service.mjs";
const require = createRequire(import.meta.url);
const kaspa = require("@kluster/kaspa-wasm");
const MAX_OPERATION_FEE = 10_000_000n;
const OPERATION_COMPUTE_BUDGET = 120;
+// Draft preflight uses zero-filled signature slots. A real CheckSig adds about
+// 2,495 compute grams with the pinned engine, or 249,500 sompi at the current
+// relay rate. Round upward so the package is fully funded before any signer is
+// asked to approve it.
+const SIGNATURE_EXECUTION_FEE_RESERVE = 250_000n;
const OPERATIONS = {
"owner-vault": [
@@ -153,7 +158,8 @@ export async function buildTemplateOperationPackage(
project,
template,
findUtxo = findCovenantUtxo,
- preflight = kascovPreflight
+ preflight = kascovPreflight,
+ feeContext = null
) {
const templateId = templateIdOf(project);
const operation = exactOperation(templateId, String(input.operationId || ""));
@@ -161,15 +167,16 @@ export async function buildTemplateOperationPackage(
if (project.deployment.network !== project.network) throw operationError("Project deployment network does not match the project");
const network = NETWORKS[project.network];
if (!network) throw operationError("Project network is unsupported");
- const source = await findUtxo(
- project.network,
- project.artifact.programHex,
- project.deployment.activeTxid || project.deployment.txid,
- project.deployment.activeOutputIndex ?? 0,
- project.deployment.covenantId || ""
- );
+ const source = feeContext?.source || await findUtxo(
+ project.network,
+ project.artifact.programHex,
+ project.deployment.activeTxid || project.deployment.txid,
+ project.deployment.activeOutputIndex ?? 0,
+ project.deployment.covenantId || ""
+ );
if (project.deployment.covenantId && project.deployment.covenantId !== source.covenantId) throw operationError("Stored deployment covenant ID does not match the unspent output");
- const fee = kasToSompi(String(input.feeKas || "0.01"));
+ const requestedFee = feeContext?.requestedFee ?? kasToSompi(String(input.feeKas || "0.01"));
+ const fee = feeContext?.fee ?? requestedFee;
if (fee < 1000n || fee > MAX_OPERATION_FEE) throw operationError("Operation fee must be from 0.00001 to 0.1 KAS/TKAS");
const inputValue = BigInt(source.entry.amount);
if (inputValue <= fee) throw operationError("Covenant value is not enough to pay the selected fee");
@@ -275,7 +282,8 @@ export async function buildTemplateOperationPackage(
previousOutpoint: source.entry.outpoint,
signatureScript: "",
sequence,
- sigOpCount: sigOps,
+ // Toccata v1 commits a compute budget, not the legacy v0 sig-op field.
+ sigOpCount: 0,
computeBudget: OPERATION_COMPUTE_BUDGET,
utxo: source.entry
}],
@@ -285,7 +293,6 @@ export async function buildTemplateOperationPackage(
gas: 0n,
payload: ""
});
- if (!kaspa.updateTransactionMass(network.kaspaNetworkId, transaction, Math.max(sigOps, 1), true)) throw operationError("Operation transaction exceeds the standard mass limit");
const packageValue = {
version: 1,
network: project.network,
@@ -308,7 +315,101 @@ export async function buildTemplateOperationPackage(
sourceSha256: project.artifact.sourceSha256 || ""
}
};
- const prepared = sigOps === 0 ? finalizeExternalCovenantPackage(packageValue) : inspectExternalCovenantPackage(packageValue);
- const preflightReport = await preflight(prepared.package.transactionSafeJson, project.network, "draft");
- return { operation, ...prepared, preflight: preflightReport };
+ let prepared = sigOps === 0 ? finalizeExternalCovenantPackage(packageValue) : inspectExternalCovenantPackage(packageValue);
+
+ // The redeem program and covenant arguments are added to signatureScript only
+ // when an operation package is finalized. Estimating before that point
+ // underprices large contracts. Fill temporary signature slots, assemble the
+ // exact script, and then calculate the network minimum fee and mass.
+ let estimation = prepared;
+ if (!prepared.review.complete) {
+ const estimatePackage = structuredClone(prepared.package);
+ for (const covenantInput of estimatePackage.covenantInputs || []) {
+ covenantInput.arguments = (covenantInput.arguments || []).map((argument) => argument?.kind === "signature" && !argument.hex
+ ? { ...argument, hex: "00".repeat(65) }
+ : argument);
+ }
+ if (estimatePackage.covenantInputs?.length === 1) estimatePackage.covenantInput = estimatePackage.covenantInputs[0];
+ estimation = finalizeExternalCovenantPackage(estimatePackage);
+ }
+ const estimatedTransaction = kaspa.Transaction.deserializeFromSafeJSON(estimation.package.transactionSafeJson);
+ const minimumSignatures = Math.max(sigOps, 1);
+ const calculatedMass = BigInt(kaspa.calculateTransactionMass(network.kaspaNetworkId, estimatedTransaction, minimumSignatures, true));
+ const maximumMass = BigInt(kaspa.maximumStandardTransactionMass());
+ const minimumFee = kaspa.calculateTransactionFee(network.kaspaNetworkId, estimatedTransaction, minimumSignatures, true);
+ try { estimatedTransaction.free?.(); } catch {}
+ if (calculatedMass > maximumMass || minimumFee === undefined) {
+ throw operationError(`Operation transaction exceeds the standard mass limit (${calculatedMass}/${maximumMass})`, "OPERATION_MASS_LIMIT");
+ }
+ const requiredFee = BigInt(minimumFee);
+ if (fee < requiredFee) {
+ const pass = Number(feeContext?.pass || 0);
+ if (pass >= 3 || requiredFee > MAX_OPERATION_FEE) {
+ throw operationError(`Operation requires at least ${sompiToKas(requiredFee)} KAS/TKAS in fees`, "OPERATION_FEE_TOO_LOW");
+ }
+ return buildTemplateOperationPackage(input, project, template, async () => source, preflight, {
+ source,
+ requestedFee,
+ fee: requiredFee,
+ pass: pass + 1
+ });
+ }
+
+ const unsignedTransaction = kaspa.Transaction.deserializeFromSafeJSON(packageValue.transactionSafeJson);
+ unsignedTransaction.storageMass = calculatedMass;
+ unsignedTransaction.finalize();
+ packageValue.transactionSafeJson = unsignedTransaction.serializeToSafeJSON();
+ try { unsignedTransaction.free?.(); } catch {}
+ prepared = sigOps === 0 ? finalizeExternalCovenantPackage(packageValue) : inspectExternalCovenantPackage(packageValue);
+ let preflightReport = await preflight(prepared.package.transactionSafeJson, project.network, "draft");
+ let engineMinimumFee = 0n;
+ try { engineMinimumFee = BigInt(preflightReport?.fee?.estimate_sompi || 0); } catch {}
+ const signatureExecutionReserve = BigInt(sigOps) * SIGNATURE_EXECUTION_FEE_RESERVE;
+ const engineRequiredFee = engineMinimumFee + signatureExecutionReserve;
+ if (engineRequiredFee > fee) {
+ const pass = Number(feeContext?.pass || 0);
+ if (pass >= 3 || engineRequiredFee > MAX_OPERATION_FEE) {
+ throw operationError(`Operation requires at least ${sompiToKas(engineRequiredFee)} KAS/TKAS in fees`, "OPERATION_FEE_TOO_LOW");
+ }
+ return buildTemplateOperationPackage(input, project, template, async () => source, preflight, {
+ source,
+ requestedFee,
+ fee: engineRequiredFee,
+ pass: pass + 1
+ });
+ }
+
+ const engineMasses = preflightReport?.masses || {};
+ const authoritativeMass = [engineMasses.compute, engineMasses.storage, engineMasses.transient]
+ .map((value) => Number(value || 0))
+ .filter((value) => Number.isSafeInteger(value) && value > 0)
+ .reduce((maximum, value) => Math.max(maximum, value), 0);
+ if (authoritativeMass > 0) {
+ const finalTransaction = kaspa.Transaction.deserializeFromSafeJSON(prepared.package.transactionSafeJson);
+ if (BigInt(finalTransaction.storageMass) !== BigInt(authoritativeMass)) {
+ finalTransaction.storageMass = BigInt(authoritativeMass);
+ finalTransaction.finalize();
+ prepared.package.transactionSafeJson = finalTransaction.serializeToSafeJSON();
+ prepared = inspectExternalCovenantPackage(prepared.package);
+ preflightReport = await preflight(prepared.package.transactionSafeJson, project.network, "draft");
+ }
+ try { finalTransaction.free?.(); } catch {}
+ }
+ return {
+ operation,
+ ...prepared,
+ preflight: preflightReport,
+ fee: {
+ requestedSompi: requestedFee.toString(),
+ requestedKas: sompiToKas(requestedFee),
+ actualSompi: fee.toString(),
+ actualKas: sompiToKas(fee),
+ automaticallyAdjusted: fee > requestedFee,
+ calculatedMass: calculatedMass.toString(),
+ maximumStandardMass: maximumMass.toString(),
+ engineMinimumFeeSompi: engineMinimumFee.toString(),
+ signatureExecutionReserveSompi: signatureExecutionReserve.toString(),
+ engineMasses
+ }
+ };
}
diff --git a/server/template-store.mjs b/server/template-store.mjs
index 063cb7b..d6da52f 100644
--- a/server/template-store.mjs
+++ b/server/template-store.mjs
@@ -37,7 +37,7 @@ function publicKeyExpression(hex) {
return compilerExpression({ kind: "pubkey", hex });
}
-function parseAmount(value, minimum = "0.05") {
+function parseAmount(value, minimum = "0.5") {
const text = String(value ?? "").trim();
if (!/^(0|[1-9]\d*)(\.\d{1,8})?$/.test(text)) throw parameterError("Template amount must be a positive KAS decimal with at most 8 places");
const [whole, fraction = ""] = text.split(".");
@@ -203,12 +203,12 @@ export class TemplateStore {
const constructorArgs = template.constructorArgs.map((value) => structuredClone(value));
const parameterEncodingVersion = Number(options.encodingVersion || template.parameterEncodingVersion || 1);
const uniqueGroups = new Map();
- let deployAmount = "0.05";
+ let deployAmount = "0.5";
for (const field of definitions) {
const raw = inputParameters?.[field.id];
if ((raw === undefined || raw === null || String(raw).trim() === "") && field.required !== false) throw parameterError(`Template parameter is required: ${field.id}`);
if (field.type === "amount") {
- const value = parseAmount(raw, field.minimum || "0.05");
+ const value = parseAmount(raw, field.minimum || "0.5");
parameters[field.id] = value;
if (field.projectField === "deployAmount") deployAmount = value;
if (Number.isInteger(field.argIndex)) {
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 26b935f..8eee447 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -66,7 +66,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "app"
-version = "0.2.6"
+version = "0.2.7"
dependencies = [
"log",
"serde",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 9088a05..08a5009 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "app"
-version = "0.2.6"
+version = "0.2.7"
description = "Local-first Kaspa SilverScript covenant workbench"
authors = ["w00c00"]
license = "MIT"
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 73bd44d..9f9614d 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Kaspa SilverScript Studio",
- "version": "0.2.6",
+ "version": "0.2.7",
"identifier": "io.kaspa.silverscript-studio",
"build": {
"frontendDist": "../dist",
diff --git a/src/main.js b/src/main.js
index 04276b4..14947b8 100644
--- a/src/main.js
+++ b/src/main.js
@@ -325,7 +325,7 @@ function loadProjectIntoUi(project) {
$("#constructor-args").value = JSON.stringify(project.constructorArgs || [], null, 2);
$("#compiler-profile").value = project.compilerProfileId || project.artifact?.compiler?.id || project.review?.compilerProfileId || state.config?.compiler?.defaultProfileId || "latest-cb34aa5";
renderCompilerProfileHelp();
- $("#deploy-amount").value = project.deployAmount || "0.05";
+ $("#deploy-amount").value = Number(project.deployAmount || 0) >= 0.5 ? project.deployAmount : "0.5";
$("#deploy-network").value = project.network || "tn10";
updateNetworkControls();
sourceStats();
@@ -350,7 +350,7 @@ function showNoProject() {
$("#requirements").value = "";
$("#source-editor").value = "";
$("#constructor-args").value = "[]";
- $("#deploy-amount").value = "0.05";
+ $("#deploy-amount").value = "0.5";
$("#no-project-banner").hidden = false;
$("#save-label").textContent = state.language === "zh" ? "没有打开的工作" : "No work open";
$("#save-dot").classList.remove("saving");
@@ -1405,7 +1405,17 @@ async function buildAndBroadcast() {
pollEvidence(result);
} catch (error) {
$("#deploy-status").textContent = "FAILED";
- toast(error.message, "bad");
+ const code = error.payload?.code;
+ const message = code === "COVENANT_AMOUNT_BELOW_STANDARD_MASS"
+ ? (state.language === "zh"
+ ? "Studio 为 Covenant 保留存储质量余量,要求至少锁定 0.5 TKAS/KAS。"
+ : "Studio requires at least 0.5 TKAS/KAS in a covenant cell to retain conservative storage-mass headroom.")
+ : code === "COVENANT_DEPLOYMENT_MASS_LIMIT"
+ ? (state.language === "zh"
+ ? "现有 UTXO 无法在标准质量上限内完成部署;请提高锁定金额或换用更合适的 UTXO。"
+ : "No available UTXO can fund this deployment within the standard mass limit; increase the locked amount or use a more suitable UTXO.")
+ : error.message;
+ toast(message, "bad");
} finally { button.disabled = false; button.textContent = tr("buildDraft"); }
}
@@ -1628,10 +1638,18 @@ async function buildLifecycleOperation() {
renderLifecycleInvitationActions();
$("#external-covenant-status").textContent = payload.review.complete ? "READY TO PREFLIGHT" : "AWAITING SIGNATURE";
$("#lifecycle-status").textContent = "PACKAGE READY";
+ const feeNotice = payload.fee?.automaticallyAdjusted
+ ? (state.language === "zh"
+ ? `已按完整脚本质量把手续费自动调整为 ${payload.fee.actualKas} ${state.project.network === "mainnet" ? "KAS" : "TKAS"}`
+ : `Fee automatically adjusted to ${payload.fee.actualKas} ${state.project.network === "mainnet" ? "KAS" : "TKAS"} from the fully assembled script mass`)
+ : "";
+ if (payload.fee?.actualKas) $("#lifecycle-fee").value = payload.fee.actualKas;
if (operation.signers) {
- toast(state.language === "zh" ? "签名邀请已生成,请点击旁边的“下载邀请文件”" : "Signing invitation created; click Download invitation beside the build button", "good");
+ const invitationNotice = state.language === "zh" ? "签名邀请已生成,请点击旁边的“下载邀请文件”" : "Signing invitation created; click Download invitation beside the build button";
+ toast(feeNotice ? `${invitationNotice} · ${feeNotice}` : invitationNotice, "good");
$("#lifecycle-download-invitation").scrollIntoView({ behavior: "smooth", block: "center" });
} else {
+ if (feeNotice) toast(feeNotice, "good");
$("#external-covenant-package").scrollIntoView({ behavior: "smooth", block: "center" });
}
return payload;
diff --git a/templates/commit-reveal/manifest.json b/templates/commit-reveal/manifest.json
index 9dac131..a2160f8 100644
--- a/templates/commit-reveal/manifest.json
+++ b/templates/commit-reveal/manifest.json
@@ -13,7 +13,7 @@
"sourceFile": "contract.sil",
"requiredReplacements": [0, 1, 2, 3, 4],
"parameters": [
- { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.05", "minimum": "0.05", "labelZh": "托管金额", "labelEn": "Escrow value", "helpZh": "Reveal 或退款时扣除显式矿工费后全额支付。", "helpEn": "Reveal or refund pays the full balance minus the explicit miner fee." },
+ { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "托管金额", "labelEn": "Escrow value", "helpZh": "至少 0.5 TKAS/KAS;Reveal 或退款时扣除显式矿工费后全额支付。", "helpEn": "At least 0.5 TKAS/KAS; reveal or refund pays the full balance minus the explicit miner fee." },
{ "id": "senderAddress", "type": "address", "argIndex": 0, "required": true, "labelZh": "发送方/退款钱包", "labelEn": "Sender / refund wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "到期后用该钱包签名退款。", "helpEn": "This wallet signs the timeout refund." },
{ "id": "recipientAddress", "type": "address", "argIndex": 1, "required": true, "labelZh": "Reveal 收款钱包", "labelEn": "Reveal recipient wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "只有该钱包签名并提供正确 payload 与 salt 才能领取。", "helpEn": "Only this wallet can claim with the correct payload and salt." },
{ "id": "domain", "type": "sha256", "argIndex": 2, "required": true, "labelZh": "协议域", "labelEn": "Protocol domain", "helpZh": "32 字节协议标识,防止承诺跨应用重放。", "helpEn": "A 32-byte protocol identifier preventing cross-application replay." },
diff --git a/templates/hashlock-refund/manifest.json b/templates/hashlock-refund/manifest.json
index 7f68d50..55f214c 100644
--- a/templates/hashlock-refund/manifest.json
+++ b/templates/hashlock-refund/manifest.json
@@ -24,7 +24,7 @@
"sourceFile": "contract.sil",
"requiredReplacements": [0, 1, 2, 3],
"parameters": [
- { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.05", "minimum": "0.05", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "创建哈希锁 Covenant UTXO 时锁定的 TKAS/KAS。", "helpEn": "TKAS/KAS locked in the hashlocked covenant UTXO." },
+ { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "至少 0.5 TKAS/KAS,以满足当前标准存储质量限制。", "helpEn": "At least 0.5 TKAS/KAS to stay within the current standard storage-mass limit." },
{ "id": "senderAddress", "type": "address", "argIndex": 0, "required": true, "useConnectedWallet": true, "labelZh": "付款/超时退款钱包", "labelEn": "Sender / timeout refund wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:..." },
{ "id": "recipientAddress", "type": "address", "argIndex": 1, "required": true, "labelZh": "收款/释放授权钱包", "labelEn": "Recipient / release wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:..." },
{ "id": "secretHash", "type": "sha256", "argIndex": 2, "required": true, "labelZh": "秘密的 SHA-256 哈希", "labelEn": "SHA-256 hash of secret", "placeholderZh": "64 位十六进制哈希,不要填写秘密原文", "placeholderEn": "64 hex characters; never enter the secret itself", "helpZh": "这里只保存哈希。秘密原文是领取凭证,必须离线保管,不能提交给应用。", "helpEn": "Only the digest is stored. The preimage authorizes claiming and must remain offline." },
diff --git a/templates/inheritance-vault/manifest.json b/templates/inheritance-vault/manifest.json
index 14a3d54..1e173e2 100644
--- a/templates/inheritance-vault/manifest.json
+++ b/templates/inheritance-vault/manifest.json
@@ -30,12 +30,12 @@
"type": "amount",
"projectField": "deployAmount",
"required": true,
- "default": "0.05",
- "minimum": "0.05",
+ "default": "0.5",
+ "minimum": "0.5",
"labelZh": "锁定金额",
"labelEn": "Locked amount",
- "helpZh": "创建继承 Covenant UTXO 时锁定的 TKAS/KAS。",
- "helpEn": "TKAS/KAS locked in the inheritance covenant UTXO."
+ "helpZh": "至少 0.5 TKAS/KAS,以满足当前标准存储质量限制。",
+ "helpEn": "At least 0.5 TKAS/KAS to stay within the current standard storage-mass limit."
},
{
"id": "ownerAddress",
diff --git a/templates/merkle-one-time-claim/manifest.json b/templates/merkle-one-time-claim/manifest.json
index 19a7f60..fa9c401 100644
--- a/templates/merkle-one-time-claim/manifest.json
+++ b/templates/merkle-one-time-claim/manifest.json
@@ -13,7 +13,7 @@
"sourceFile": "contract.sil",
"requiredReplacements": [0, 1, 2, 3, 4, 5],
"parameters": [
- { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.05", "minimum": "0.05", "labelZh": "领取资金", "labelEn": "Claim value", "helpZh": "扣除显式矿工费后的余额一次性支付给领取人。", "helpEn": "The full balance minus the explicit miner fee is paid once to the claimant." },
+ { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "领取资金", "labelEn": "Claim value", "helpZh": "至少 0.5 TKAS/KAS;扣除显式矿工费后的余额一次性支付给领取人。", "helpEn": "At least 0.5 TKAS/KAS; the full balance minus the explicit miner fee is paid once to the claimant." },
{ "id": "claimantAddress", "type": "address", "argIndex": 0, "required": true, "labelZh": "领取钱包", "labelEn": "Claimant wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "Merkle 叶子和链上签名同时绑定该钱包。", "helpEn": "Both the Merkle leaf and on-chain signature bind this wallet." },
{ "id": "refundAddress", "type": "address", "argIndex": 1, "required": true, "labelZh": "超时退款钱包", "labelEn": "Timeout refund wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "到期后只有该钱包能退款。", "helpEn": "Only this wallet can refund after timeout." },
{ "id": "merkleRoot", "type": "sha256", "argIndex": 2, "required": true, "labelZh": "Merkle Root", "labelEn": "Merkle root", "helpZh": "叶子为 SHA256(claimantPubkey || claimId || salt32)。", "helpEn": "Leaf is SHA256(claimantPubkey || claimId || salt32)." },
diff --git a/templates/owner-vault/manifest.json b/templates/owner-vault/manifest.json
index 7eba37a..8a3a627 100644
--- a/templates/owner-vault/manifest.json
+++ b/templates/owner-vault/manifest.json
@@ -24,7 +24,7 @@
"sourceFile": "contract.sil",
"requiredReplacements": [0],
"parameters": [
- { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.05", "minimum": "0.05", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "创建 Covenant UTXO 时实际锁定的 TKAS/KAS。", "helpEn": "TKAS/KAS locked when the covenant UTXO is created." },
+ { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "至少 0.5 TKAS/KAS,以满足当前标准存储质量限制。", "helpEn": "At least 0.5 TKAS/KAS to stay within the current standard storage-mass limit." },
{ "id": "ownerAddress", "type": "address", "argIndex": 0, "required": true, "useConnectedWallet": true, "labelZh": "释放授权钱包", "labelEn": "Release authorization wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "只有这个 P2PK 钱包可以签名释放资金;签名前仍需核对实际输出地址。", "helpEn": "Only this P2PK wallet can authorize release; still verify the actual output address before signing." }
],
"constructorArgs": [{ "kind": "pubkey", "hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" }],
diff --git a/templates/timelock-transfer/manifest.json b/templates/timelock-transfer/manifest.json
index 8d502a6..90a38a5 100644
--- a/templates/timelock-transfer/manifest.json
+++ b/templates/timelock-transfer/manifest.json
@@ -24,7 +24,7 @@
"sourceFile": "contract.sil",
"requiredReplacements": [0, 1, 2],
"parameters": [
- { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.05", "minimum": "0.05", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "创建 Covenant UTXO 时实际锁定的 TKAS/KAS。", "helpEn": "TKAS/KAS locked when the covenant UTXO is created." },
+ { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "至少 0.5 TKAS/KAS,以满足当前标准存储质量限制。", "helpEn": "At least 0.5 TKAS/KAS to stay within the current standard storage-mass limit." },
{ "id": "senderAddress", "type": "address", "argIndex": 0, "required": true, "useConnectedWallet": true, "labelZh": "付款/超时退款钱包", "labelEn": "Sender / timeout refund wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "超时后只有这个 P2PK 钱包可以签名退款。", "helpEn": "After timeout, only this P2PK wallet can authorize a refund." },
{ "id": "recipientAddress", "type": "address", "argIndex": 1, "required": true, "labelZh": "收款/释放授权钱包", "labelEn": "Recipient / release wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "这个 P2PK 钱包可以在超时前签名领取;签名前仍需核对实际输出。", "helpEn": "This P2PK wallet can authorize a claim before timeout; verify the actual output before signing." },
{ "id": "unlockAt", "type": "datetime", "argIndex": 2, "required": true, "defaultOffsetSeconds": 86400, "quickOffsets": [60, 600, 3600, 86400], "labelZh": "退款解锁时间", "labelEn": "Refund unlock time", "helpZh": "本地转换为 SilverScript tx.time 使用的 Unix 时间戳;+1 分钟仅用于 TN10 测试。", "helpEn": "Converted locally to the Unix timestamp used by SilverScript tx.time; +1 minute is for TN10 testing only." }
diff --git a/templates/two-of-three/manifest.json b/templates/two-of-three/manifest.json
index eb78d1c..bff9e48 100644
--- a/templates/two-of-three/manifest.json
+++ b/templates/two-of-three/manifest.json
@@ -24,7 +24,7 @@
"sourceFile": "contract.sil",
"requiredReplacements": [0, 1, 2],
"parameters": [
- { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.05", "minimum": "0.05", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "创建多签 Covenant UTXO 时锁定的 TKAS/KAS。", "helpEn": "TKAS/KAS locked in the multisig covenant UTXO." },
+ { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "至少 0.5 TKAS/KAS,以满足当前标准存储质量限制。", "helpEn": "At least 0.5 TKAS/KAS to stay within the current standard storage-mass limit." },
{ "id": "key1Address", "type": "address", "argIndex": 0, "uniqueGroup": "signers", "required": true, "useConnectedWallet": true, "labelZh": "签名钱包 1", "labelEn": "Signer wallet 1", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "三个不同钱包中任意两个必须共同授权。", "helpEn": "Any two of three distinct wallets must authorize together." },
{ "id": "key2Address", "type": "address", "argIndex": 1, "uniqueGroup": "signers", "required": true, "labelZh": "签名钱包 2", "labelEn": "Signer wallet 2", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:..." },
{ "id": "key3Address", "type": "address", "argIndex": 2, "uniqueGroup": "signers", "required": true, "labelZh": "签名钱包 3", "labelEn": "Signer wallet 3", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:..." }
diff --git a/test/studio.test.mjs b/test/studio.test.mjs
index d650c49..48f0927 100644
--- a/test/studio.test.mjs
+++ b/test/studio.test.mjs
@@ -156,6 +156,22 @@ test("desktop helpers use native executable names on Windows and Unix", () => {
assert.match(rustLauncher, /cfg!\(windows\)/);
});
+test("local preflight builds from the committed MIT source snapshot", () => {
+ const buildScript = fs.readFileSync(new URL("../scripts/build-kascov-preflight.mjs", import.meta.url), "utf8");
+ const provenance = JSON.parse(fs.readFileSync(new URL("../config/kascov-preflight.json", import.meta.url), "utf8"));
+ assert.equal(provenance.sourceMode, "vendored-mit-snapshot");
+ assert.match(buildScript, /vendor[^\n]+kascov-preflight/);
+ assert.doesNotMatch(buildScript, /KASCOV_REPOSITORY|KASCOV_COMMIT|git[^\n]+(?:clone|fetch)/);
+ for (const requiredPath of [
+ "../vendor/kascov-preflight/Cargo.toml",
+ "../vendor/kascov-preflight/Cargo.lock",
+ "../vendor/kascov-preflight/LICENSE",
+ "../vendor/kascov-preflight/crates/studio-kascov-preflight/src/preflight.rs"
+ ]) {
+ assert.equal(fs.existsSync(new URL(requiredPath, import.meta.url)), true, `${requiredPath} must be committed`);
+ }
+});
+
test("AI package retains explicit transaction plans and stays experimental", () => {
const parsed = parseAiContract(JSON.stringify({
specification: { title: "Counter" },
@@ -290,7 +306,7 @@ test("a deterministic template can replace a local work without AI and clears st
assert.equal(applied.id, project.id);
assert.equal(applied.review.templateId, template.id);
assert.equal(applied.source, template.source);
- assert.equal(applied.deployAmount, "0.15");
+ assert.equal(applied.deployAmount, "0.5");
assert.deepEqual(applied.templateParameters, parameters);
assert.equal(applied.artifact, null);
assert.equal(applied.deployment, null);
@@ -304,7 +320,7 @@ test("human template fields deterministically produce compile-ready constructor
for (const template of templates.list()) {
const parameters = configuredTemplateParameters(template);
const input = templates.projectInput(template.id, "tn10", parameters);
- assert.equal(input.deployAmount, parameters.amountKas || "0.15");
+ assert.equal(input.deployAmount, parameters.amountKas || "0.5");
if (template.id === "kcc721-experimental") {
assert.equal(input.templateParameters.collectionMode, "preview");
assert.equal(input.templateParameters.collectionId, null);
@@ -437,6 +453,12 @@ test("phrase-free local renewal requires the exact TN10 project, artifact, outpo
}
};
assert.equal(assertLocalRenewalPackage(project, inspected), true);
+ const dynamicFee = structuredClone(inspected);
+ dynamicFee.review.feeSompi = "1460500";
+ assert.equal(assertLocalRenewalPackage(project, dynamicFee), true);
+ const excessiveFee = structuredClone(inspected);
+ excessiveFee.review.feeSompi = "2000001";
+ assert.throws(() => assertLocalRenewalPackage(project, excessiveFee), /0\.00001 to 0\.02 TKAS/i);
const counterfeit = structuredClone(inspected);
counterfeit.review.outputs[0].covenantId = "66".repeat(32);
assert.throws(() => assertLocalRenewalPackage(project, counterfeit), /exactly one same-covenant/i);
@@ -587,7 +609,13 @@ test("mature inheritance builds a complete unsigned distribution that conserves
payload: ""
}));
const lookup = async () => ({ entry: holder.inputs[0].utxo, address: p2shAddress, covenantId });
- const preflight = async () => ({ ok: true, verdict: "ready", stage: "draft" });
+ const preflight = async () => ({
+ ok: true,
+ verdict: "ready",
+ stage: "draft",
+ fee: { estimate_sompi: 1_740_100 },
+ masses: { compute: 15_401, storage: 61_632, transient: 10_724 }
+ });
const built = await buildTemplateOperationPackage(
{ operationId: "inherit", feeKas: "0.01" },
@@ -601,15 +629,19 @@ test("mature inheritance builds a complete unsigned distribution that conserves
assert.equal(built.review.operation.kind, "inheritance-payment");
assert.equal(built.review.complete, true);
assert.deepEqual(built.review.signatureSlots, []);
- assert.deepEqual(built.review.outputs.map((output) => output.valueSompi), ["29400000", "19600000"]);
+ assert.deepEqual(built.review.outputs.map((output) => output.valueSompi), ["28955940", "19303960"]);
assert.deepEqual(
built.review.outputs.map((output) => output.address),
configured.templateParameters.inheritors.map((inheritor) => inheritor.address.toLowerCase())
);
assert.equal(
built.review.outputs.reduce((total, output) => total + BigInt(output.valueSompi), 0n),
- 49_000_000n
+ 48_259_900n
);
+ assert.equal(built.fee.requestedSompi, "1000000");
+ assert.equal(built.fee.actualSompi, "1740100");
+ assert.equal(built.fee.automaticallyAdjusted, true);
+ assert.equal(JSON.parse(built.package.transactionSafeJson).storageMass, "61632");
assert.equal(
JSON.parse(built.package.transactionSafeJson).inputs[0].sequence,
String(configured.constructorArgs[3].data)
@@ -1082,6 +1114,7 @@ test("TN10 Experimental KCC721 pack compiles all pinned contracts and blocks sta
feeSompi: "1000000",
provenance: { templateId: pack.id, operationId: "transfer" }
});
+ assert.ok(JSON.parse(atomic.transactionSafeJson).inputs.every((input) => input.sigOpCount === 0));
const p2pkSigner = {
async signP2pkInput({ transactionSafeJson, inputIndex }) {
const transaction = kaspa.Transaction.deserializeFromSafeJSON(transactionSafeJson);
@@ -1160,16 +1193,18 @@ test("every built-in template exposes a deterministic reverse operation package"
const preflight = async (transactionSafeJson, network, stage) => {
const transaction = JSON.parse(transactionSafeJson);
assert.equal(transaction.inputs[0].computeBudget, 120, templateId);
+ assert.equal(transaction.inputs[0].sigOpCount, 0, `${templateId} must not carry a legacy v0 sig-op count in Toccata v1`);
assert.equal(network, "tn10", templateId);
assert.equal(stage, "draft", templateId);
- return { ok: true, verdict: "ready", stage };
+ return { ok: true, verdict: "ready", stage, fee: { estimate_sompi: 1_000_000 } };
};
const built = await buildTemplateOperationPackage(operationInput, project, template, lookup, preflight);
assert.equal(built.review.entrypoint, operationInput.operationId, templateId);
assert.equal(built.review.covenantId, covenantId, templateId);
const operations = templateOperations(project);
assert.ok(operations.length >= 1, templateId);
- assert.equal(built.review.feeSompi, "1000000", templateId);
+ assert.equal(built.review.feeSompi, templateId === "two-of-three" ? "1500000" : "1250000", templateId);
+ assert.equal(built.fee.signatureExecutionReserveSompi, templateId === "two-of-three" ? "500000" : "250000", templateId);
assert.equal(built.preflight.verdict, "ready", templateId);
if (templateId === "two-of-three") {
assert.deepEqual(operations[0].availableSigners, [
@@ -1251,7 +1286,7 @@ test("deployment builder rejects source edited after compilation before network
constructorArgsSha256: crypto.createHash("sha256").update("[]").digest("hex")
};
await assert.rejects(
- buildDeployDraft({ network: "tn10", address, publicKey, amountKas: "0.05", artifact, source: "edited source", constructorArgs: [] }, {}),
+ buildDeployDraft({ network: "tn10", address, publicKey, amountKas: "0.5", artifact, source: "edited source", constructorArgs: [] }, {}),
/source changed after compilation/i
);
});
@@ -1267,11 +1302,69 @@ test("deployment builder rejects constructor arguments edited after compilation
constructorArgsSha256: crypto.createHash("sha256").update("[]").digest("hex")
};
await assert.rejects(
- buildDeployDraft({ network: "tn10", address, publicKey, amountKas: "0.05", artifact, source: "compiled source", constructorArgs: [{ kind: "int", data: 1 }] }, {}),
+ buildDeployDraft({ network: "tn10", address, publicKey, amountKas: "0.5", artifact, source: "compiled source", constructorArgs: [{ kind: "int", data: 1 }] }, {}),
/constructor arguments changed after compilation/i
);
});
+test("deployment builder enforces a standard-mass-safe covenant cell and accepts a large faucet UTXO", async () => {
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "silverstudio-deploy-mass-test-"));
+ const originalFetch = globalThis.fetch;
+ const privateKey = new kaspa.PrivateKey("04".padStart(64, "0"));
+ const publicKey = privateKey.toPublicKey().toXOnlyPublicKey().toString();
+ const address = privateKey.toAddress("testnet-10").toString();
+ const script = kaspa.payToAddressScript(address).script;
+ const source = "compiled source";
+ const constructorArgs = [];
+ const programHex = "51";
+ const artifact = {
+ programHex,
+ programSha256: crypto.createHash("sha256").update(Buffer.from(programHex, "hex")).digest("hex"),
+ sourceSha256: crypto.createHash("sha256").update(source).digest("hex"),
+ constructorArgsSha256: crypto.createHash("sha256").update(JSON.stringify(constructorArgs)).digest("hex")
+ };
+ try {
+ await assert.rejects(
+ buildDeployDraft({ network: "tn10", address, publicKey, amountKas: "0.05", artifact, source, constructorArgs }, new DraftStore(directory)),
+ (error) => error.code === "COVENANT_AMOUNT_BELOW_STANDARD_MASS" && error.report?.minimumAmountKas === "0.5"
+ );
+ setRpcClientFactoryForTests(() => ({
+ async connect() {},
+ async disconnect() {},
+ async stop() {},
+ async getServerInfo() { return { networkId: "testnet-10" }; },
+ async getUtxosByAddresses() {
+ return { entries: [{
+ address,
+ outpoint: { transactionId: "44".repeat(32), index: 0 },
+ amount: 4000n * 100_000_000n,
+ scriptPublicKey: { script },
+ blockDaaScore: 0n,
+ isCoinbase: false
+ }] };
+ }
+ }));
+ globalThis.fetch = async () => new Response(JSON.stringify({ ok: true, verdict: "ready", findings: [] }), {
+ status: 200,
+ headers: { "content-type": "application/json" }
+ });
+ const draft = await buildDeployDraft(
+ { network: "tn10", address, publicKey, amountKas: "0.5", artifact, source, constructorArgs },
+ new DraftStore(directory)
+ );
+ const transaction = kaspa.Transaction.deserializeFromSafeJSON(draft.signing.txJsonString);
+ assert.equal(draft.amountSompi, "50000000");
+ assert.equal(transaction.outputs[0].value, 50_000_000n);
+ assert.ok(BigInt(transaction.storageMass) <= BigInt(kaspa.maximumStandardTransactionMass()));
+ try { transaction.free(); } catch {}
+ } finally {
+ setRpcClientFactoryForTests();
+ globalThis.fetch = originalFetch;
+ try { privateKey.free(); } catch {}
+ fs.rmSync(directory, { recursive: true, force: true });
+ }
+});
+
test("Kascov preflight adapter preserves WASM Safe JSON outpoints, scripts and genesis covenant identity", () => {
const privateKey = new kaspa.PrivateKey("01".padStart(64, "0"));
const address = privateKey.toAddress("testnet-10").toString();
@@ -1458,6 +1551,9 @@ test("every built-in template fully compiles with its realistic default argument
const templates = new TemplateStore().list();
assert.ok(templates.length >= 4);
for (const template of templates) {
+ const amount = template.parameters.find((field) => field.type === "amount");
+ assert.ok(!amount || Number(amount.default) >= 0.5, `${template.id} default covenant cell is below the standard-mass-safe minimum`);
+ assert.ok(!amount || Number(amount.minimum) >= 0.5, `${template.id} minimum covenant cell is below the standard-mass-safe minimum`);
const artifact = await compileContract(template);
assert.ok(artifact.programHex.length > 0, template.id);
assert.equal(artifact.compiler.upstreamCommit, SILVERSCRIPT_COMMIT);
@@ -1487,7 +1583,7 @@ test("local wallet encrypts its mnemonic and signs without persisting secrets",
};
const transaction = new kaspa.Transaction({
version: 1,
- inputs: [{ previousOutpoint: utxo.outpoint, signatureScript: "", sequence: 0n, sigOpCount: 1, utxo }],
+ inputs: [{ previousOutpoint: utxo.outpoint, signatureScript: "", sequence: 0n, sigOpCount: 0, computeBudget: 10, utxo }],
outputs: [new kaspa.TransactionOutput(99_000_000n, kaspa.payToAddressScript(wallet.address))],
lockTime: 0n,
subnetworkId: "00".repeat(20),
diff --git a/vendor/kascov-preflight/Cargo.lock b/vendor/kascov-preflight/Cargo.lock
new file mode 100644
index 0000000..709fd5d
--- /dev/null
+++ b/vendor/kascov-preflight/Cargo.lock
@@ -0,0 +1,3703 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+
+[[package]]
+name = "arc-swap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "ark-bn254"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bc66f96ebe2a17a499475b4f94791d379817592ef494171586967ffdc6f95db"
+dependencies = [
+ "ark-ec",
+ "ark-ff",
+ "ark-std",
+]
+
+[[package]]
+name = "ark-crypto-primitives"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b3409b1846fe459d19c95df039481575ac6d5842ae63858ad75cc31219bfc1"
+dependencies = [
+ "ahash",
+ "ark-crypto-primitives-macros",
+ "ark-ec",
+ "ark-ff",
+ "ark-r1cs-std",
+ "ark-relations",
+ "ark-serialize",
+ "ark-snark",
+ "ark-std",
+ "blake2",
+ "blake3",
+ "derivative",
+ "digest",
+ "fnv",
+ "merlin",
+ "num-bigint",
+ "rayon",
+ "sha2",
+]
+
+[[package]]
+name = "ark-crypto-primitives-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "ark-ec"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8352a2b2aedf6ba2cc38f7520fc51191d518dde96175c729af19f2d059f191c4"
+dependencies = [
+ "ahash",
+ "ark-ff",
+ "ark-poly",
+ "ark-serialize",
+ "ark-std",
+ "educe",
+ "fnv",
+ "hashbrown 0.17.1",
+ "itertools 0.14.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "rayon",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af"
+dependencies = [
+ "ark-ff-asm",
+ "ark-ff-macros",
+ "ark-serialize",
+ "ark-std",
+ "digest",
+ "educe",
+ "num-bigint",
+ "num-traits",
+ "rayon",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2"
+dependencies = [
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "ark-groth16"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a293328aa422e65527e285614ce5d1dceb0bd7b8b18d18b1b63191ee1f74cb41"
+dependencies = [
+ "ark-crypto-primitives",
+ "ark-ec",
+ "ark-ff",
+ "ark-poly",
+ "ark-relations",
+ "ark-serialize",
+ "ark-snark",
+ "ark-std",
+ "rayon",
+]
+
+[[package]]
+name = "ark-poly"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f55af10b672002b8d953e230282c51206842e20e5791a94432219b4201de5c"
+dependencies = [
+ "ahash",
+ "ark-ff",
+ "ark-serialize",
+ "ark-std",
+ "educe",
+ "fnv",
+ "hashbrown 0.17.1",
+ "rayon",
+]
+
+[[package]]
+name = "ark-r1cs-std"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291f1c6628bfcac79b0dc2adbe401aa9100e2e96daa971645e0b18fc94de9a98"
+dependencies = [
+ "ark-ec",
+ "ark-ff",
+ "ark-relations",
+ "ark-std",
+ "educe",
+ "itertools 0.14.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "tracing",
+]
+
+[[package]]
+name = "ark-relations"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4c11c797a64b8a23e22bf4e77bf582ac27bb21395e3183a9a506ba2561e9f9"
+dependencies = [
+ "ark-ff",
+ "ark-poly",
+ "ark-serialize",
+ "ark-std",
+ "foldhash 0.1.5",
+ "indexmap 2.14.0",
+ "rayon",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b"
+dependencies = [
+ "ark-serialize-derive",
+ "ark-std",
+ "digest",
+ "num-bigint",
+ "rayon",
+ "serde_with",
+]
+
+[[package]]
+name = "ark-serialize-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "ark-snark"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bdb461d2be9b2bd6f303c79fffc89f5858790a7b4d33257bca3178e2c071fb9"
+dependencies = [
+ "ark-ff",
+ "ark-relations",
+ "ark-serialize",
+ "ark-std",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155"
+dependencies = [
+ "num-traits",
+ "rand 0.8.6",
+ "rayon",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
+
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener 5.4.2",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-std"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
+dependencies = [
+ "async-attributes",
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "blake2b_simd"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "constant_time_eq",
+]
+
+[[package]]
+name = "blake3"
+version = "1.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.0",
+]
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "borsh"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145"
+dependencies = [
+ "borsh-derive",
+ "bytes",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "camino"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "console"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
+dependencies = [
+ "dispatch2",
+ "nix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.118",
+ "unicode-xid",
+]
+
+[[package]]
+name = "destructure_traitobject"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "downcast"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "elf"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b"
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener 5.4.2",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "faster-hex"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hex-literal"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
+
+[[package]]
+name = "hexplay"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da1f4f846e8dcc1b5225caf702924816cabd855e4b46115c334ba09d5254a21"
+dependencies = [
+ "atty",
+ "termcolor",
+]
+
+[[package]]
+name = "humantime"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kascov-decode"
+version = "0.1.0"
+dependencies = [
+ "blake2b_simd",
+ "hex",
+ "serde",
+]
+
+[[package]]
+name = "kascov-sim"
+version = "0.1.0"
+dependencies = [
+ "blake2b_simd",
+ "hex",
+ "kascov-decode",
+ "kaspa-addresses",
+ "kaspa-consensus-core",
+ "kaspa-txscript",
+ "secp256k1",
+ "serde",
+]
+
+[[package]]
+name = "kaspa-addresses"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "borsh",
+ "js-sys",
+ "serde",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "workflow-log",
+ "workflow-wasm",
+]
+
+[[package]]
+name = "kaspa-consensus-core"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "arc-swap",
+ "async-trait",
+ "bitflags 2.13.0",
+ "borsh",
+ "cfg-if",
+ "faster-hex",
+ "futures-util",
+ "getrandom 0.2.17",
+ "itertools 0.13.0",
+ "js-sys",
+ "kaspa-addresses",
+ "kaspa-core",
+ "kaspa-hashes",
+ "kaspa-math",
+ "kaspa-merkle",
+ "kaspa-muhash",
+ "kaspa-smt",
+ "kaspa-txscript-errors",
+ "kaspa-utils",
+ "rand 0.8.6",
+ "secp256k1",
+ "serde",
+ "serde-value",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "workflow-core",
+ "workflow-log",
+ "workflow-serializer",
+ "workflow-wasm",
+]
+
+[[package]]
+name = "kaspa-core"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "ctrlc",
+ "downcast",
+ "futures-util",
+ "log",
+ "log4rs",
+ "num_cpus",
+ "thiserror 2.0.18",
+ "tokio",
+ "triggered",
+ "wasm-bindgen",
+ "workflow-log",
+]
+
+[[package]]
+name = "kaspa-hashes"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "blake2b_simd",
+ "blake3",
+ "borsh",
+ "cc",
+ "faster-hex",
+ "kaspa-utils",
+ "keccak",
+ "serde",
+ "sha2",
+ "sha2-const-stable",
+ "wasm-bindgen",
+ "zerocopy",
+]
+
+[[package]]
+name = "kaspa-math"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "borsh",
+ "faster-hex",
+ "js-sys",
+ "kaspa-utils",
+ "malachite-base",
+ "malachite-nz",
+ "serde",
+ "serde-wasm-bindgen",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "workflow-core",
+ "workflow-log",
+ "workflow-wasm",
+]
+
+[[package]]
+name = "kaspa-merkle"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "kaspa-hashes",
+]
+
+[[package]]
+name = "kaspa-muhash"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "kaspa-hashes",
+ "kaspa-math",
+ "rand_chacha 0.3.1",
+ "serde",
+]
+
+[[package]]
+name = "kaspa-smt"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "blake3",
+ "kaspa-hashes",
+ "thiserror 2.0.18",
+ "zerocopy",
+]
+
+[[package]]
+name = "kaspa-txscript"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "ark-bn254",
+ "ark-ec",
+ "ark-groth16",
+ "ark-relations",
+ "ark-serialize",
+ "ark-snark",
+ "blake2b_simd",
+ "blake3",
+ "borsh",
+ "cfg-if",
+ "faster-hex",
+ "hexplay",
+ "indexmap 2.14.0",
+ "itertools 0.13.0",
+ "kaspa-addresses",
+ "kaspa-consensus-core",
+ "kaspa-hashes",
+ "kaspa-txscript-errors",
+ "kaspa-utils",
+ "log",
+ "parking_lot",
+ "rand 0.8.6",
+ "risc0-binfmt",
+ "risc0-circuit-recursion",
+ "risc0-core",
+ "risc0-zkp",
+ "secp256k1",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "workflow-wasm",
+]
+
+[[package]]
+name = "kaspa-txscript-errors"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "borsh",
+ "kaspa-hashes",
+ "secp256k1",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "kaspa-utils"
+version = "2.0.1"
+source = "git+https://github.com/kaspanet/rusty-kaspa?rev=98a4ccd8d200853787f227bd4536ac540cf34957#98a4ccd8d200853787f227bd4536ac540cf34957"
+dependencies = [
+ "async-channel 2.5.0",
+ "borsh",
+ "cfg-if",
+ "faster-hex",
+ "ipnet",
+ "itertools 0.13.0",
+ "log",
+ "once_cell",
+ "parking_lot",
+ "serde",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.18",
+ "uuid",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+dependencies = [
+ "serde_core",
+ "value-bag",
+]
+
+[[package]]
+name = "log-mdc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
+
+[[package]]
+name = "log4rs"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c"
+dependencies = [
+ "anyhow",
+ "arc-swap",
+ "chrono",
+ "derive_more",
+ "flate2",
+ "fnv",
+ "humantime",
+ "libc",
+ "log",
+ "log-mdc",
+ "mock_instant",
+ "parking_lot",
+ "rand 0.9.4",
+ "serde",
+ "serde-value",
+ "serde_json",
+ "serde_yaml",
+ "thiserror 2.0.18",
+ "thread-id",
+ "typemap-ors",
+ "unicode-segmentation",
+ "winapi",
+]
+
+[[package]]
+name = "malachite-base"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4f44099731f17094b07825c88ccb5fbd1bfa1f82fafff7daa33e8b8652db16e"
+dependencies = [
+ "hashbrown 0.16.1",
+ "itertools 0.14.0",
+ "libm",
+ "ryu",
+]
+
+[[package]]
+name = "malachite-nz"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a137660cdba20f136c8a223125f08088adb4e0b72fbb8466f08c43e31cc0427d"
+dependencies = [
+ "itertools 0.14.0",
+ "libm",
+ "malachite-base",
+ "wide",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "merlin"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d"
+dependencies = [
+ "byteorder",
+ "keccak",
+ "rand_core 0.6.4",
+ "zeroize",
+]
+
+[[package]]
+name = "metal"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
+dependencies = [
+ "bitflags 2.13.0",
+ "block",
+ "core-graphics-types",
+ "foreign-types",
+ "log",
+ "objc",
+ "paste",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mock_instant"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb517913cfcfb9eeda59f36020269075a152701a01606c612f547e4890be399"
+
+[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags 2.13.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi 0.5.2",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-float"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "parse-variants"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b4d1bb0b90012ce8056bd6bb5168d8de026d633dd592a732da5a25d3f6c74c"
+dependencies = [
+ "parse-variants-derive",
+]
+
+[[package]]
+name = "parse-variants-derive"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d80829147ec6f1b27109c7daaea62fc3a21a0348cdddf22b4093f0c35ab25a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi 0.5.2",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proptest"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
+dependencies = [
+ "bitflags 2.13.0",
+ "num-traits",
+ "rand 0.9.4",
+ "rand_chacha 0.9.0",
+ "rand_xorshift",
+ "unarray",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_xorshift"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+dependencies = [
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "risc0-binfmt"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1883f0c5d19b865f395209a137dcb29e56dc49951424967b8d0114c129f46e77"
+dependencies = [
+ "anyhow",
+ "borsh",
+ "bytemuck",
+ "derive_more",
+ "elf",
+ "lazy_static",
+ "postcard",
+ "risc0-zkp",
+ "risc0-zkvm-platform",
+ "ruint",
+ "semver",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "risc0-circuit-recursion"
+version = "4.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2347e909c6b2a65584b5898f3802eec5b8c1b4b45329edfdd8587b6a04dd3357"
+dependencies = [
+ "anyhow",
+ "bytemuck",
+ "hex",
+ "metal",
+ "risc0-core",
+ "risc0-zkp",
+ "tracing",
+]
+
+[[package]]
+name = "risc0-core"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b956a976b8ce4713694dcc6c370b522a42ccef4ba45da5b6e57dbf26cdb7b1"
+dependencies = [
+ "bytemuck",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "risc0-zkp"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f40d362a6c146ec6dc69208f539b92fd86e47b0dbc2083801423034a38155a2"
+dependencies = [
+ "anyhow",
+ "blake2",
+ "borsh",
+ "bytemuck",
+ "cfg-if",
+ "digest",
+ "hex",
+ "hex-literal",
+ "metal",
+ "paste",
+ "rand_core 0.9.5",
+ "risc0-core",
+ "risc0-zkvm-platform",
+ "serde",
+ "sha2",
+ "stability",
+ "tracing",
+]
+
+[[package]]
+name = "risc0-zkvm-platform"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4db893788c416287e2e1a87e6b8f5302511a04a45329e699d6a32a16874fd24f"
+dependencies = [
+ "cfg-if",
+ "num_enum",
+ "paste",
+ "stability",
+]
+
+[[package]]
+name = "rlimit"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ruint"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970"
+dependencies = [
+ "borsh",
+ "proptest",
+ "rand 0.8.6",
+ "rand 0.9.4",
+ "ruint-macro",
+ "serde_core",
+ "valuable",
+ "zeroize",
+]
+
+[[package]]
+name = "ruint-macro"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "safe_arch"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "secp256k1"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+dependencies = [
+ "rand 0.8.6",
+ "secp256k1-sys",
+ "serde",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+dependencies = [
+ "base64",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "time",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap 2.14.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "sha2-const-stable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
+
+[[package]]
+name = "stability"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac"
+dependencies = [
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "studio-kascov-preflight"
+version = "0.1.0"
+dependencies = [
+ "blake2b_simd",
+ "hex",
+ "kascov-decode",
+ "kascov-sim",
+ "kaspa-consensus-core",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "thread-id"
+version = "5.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2010d27add3f3240c1fef7959f46c814487b216baee662af53be645ba7831c07"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
+dependencies = [
+ "deranged",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "triggered"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "593eddbc8a11f3e099e942c8c065fe376b9d1776741430888f2796682e08ab43"
+
+[[package]]
+name = "typemap-ors"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867"
+dependencies = [
+ "unsafe-any-ors",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "unsafe-any-ors"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad"
+dependencies = [
+ "destructure_traitobject",
+]
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "uuid"
+version = "1.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "rand 0.10.1",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "value-bag"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
+
+[[package]]
+name = "vergen"
+version = "8.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566"
+dependencies = [
+ "anyhow",
+ "cargo_metadata",
+ "cfg-if",
+ "regex",
+ "rustc_version",
+ "rustversion",
+ "time",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wide"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "workflow-core"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d67bbe225ea90aa6979167f28935275506696ac867661e218893d3a42e1666"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-std",
+ "borsh",
+ "bs58",
+ "cfg-if",
+ "chrono",
+ "dirs",
+ "faster-hex",
+ "futures",
+ "getrandom 0.2.17",
+ "instant",
+ "js-sys",
+ "rand 0.8.6",
+ "rlimit",
+ "serde",
+ "serde-wasm-bindgen",
+ "thiserror 1.0.69",
+ "tokio",
+ "triggered",
+ "vergen",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "workflow-core-macros",
+ "workflow-log",
+]
+
+[[package]]
+name = "workflow-core-macros"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65659ed208b0066a9344142218abda353eb6c6cc1fc3ae4808b750c560de004b"
+dependencies = [
+ "convert_case",
+ "parse-variants",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "sha2",
+ "syn 1.0.109",
+ "workflow-macro-tools",
+]
+
+[[package]]
+name = "workflow-log"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64bf52c539193f219b7a79eb0c7c5f6c222ccf9b95c5e0bd59e924feb762256f"
+dependencies = [
+ "cfg-if",
+ "console",
+ "downcast",
+ "hexplay",
+ "lazy_static",
+ "log",
+ "termcolor",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "workflow-macro-tools"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "085d3045d5ca780fb589d230030e34fec962b3638d6c69806a72a7d7d1affea4"
+dependencies = [
+ "convert_case",
+ "parse-variants",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "workflow-panic-hook"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c76ca8b459e4f0c949f06ce2d45565a6769748e83ca7064d36671bbd67b4da"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "workflow-serializer"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64679db6856852a472caff4ce869e3ecebe291fbccc9406e9643eb5951a0904a"
+dependencies = [
+ "ahash",
+ "borsh",
+ "serde",
+]
+
+[[package]]
+name = "workflow-wasm"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799e5fbf266e0fffb5c24d6103735eb2b94bb31f93b664b91eaaf63b4f959804"
+dependencies = [
+ "cfg-if",
+ "faster-hex",
+ "futures",
+ "js-sys",
+ "serde",
+ "serde-wasm-bindgen",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "workflow-core",
+ "workflow-log",
+ "workflow-panic-hook",
+ "workflow-wasm-macros",
+]
+
+[[package]]
+name = "workflow-wasm-macros"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40237c65ecff78dbfedb13985e33f802a31f6f7de72dff12a6674fcdcf601822"
+dependencies = [
+ "js-sys",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/vendor/kascov-preflight/Cargo.toml b/vendor/kascov-preflight/Cargo.toml
new file mode 100644
index 0000000..a2a73b0
--- /dev/null
+++ b/vendor/kascov-preflight/Cargo.toml
@@ -0,0 +1,32 @@
+[workspace]
+resolver = "2"
+members = [
+ "crates/kascov-decode",
+ "crates/kascov-sim",
+ "crates/studio-kascov-preflight",
+]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+repository = "https://github.com/Knitser/kascov"
+
+[workspace.dependencies]
+kascov-decode = { path = "crates/kascov-decode" }
+kascov-sim = { path = "crates/kascov-sim" }
+
+# Keep the exact Toccata-aware node revision used by the audited Kascov
+# snapshot. Runtime preflight is local; this dependency is only fetched when
+# rebuilding the bundled helper from source.
+kaspa-consensus-core = { git = "https://github.com/kaspanet/rusty-kaspa", rev = "98a4ccd8d200853787f227bd4536ac540cf34957" }
+kaspa-addresses = { git = "https://github.com/kaspanet/rusty-kaspa", rev = "98a4ccd8d200853787f227bd4536ac540cf34957" }
+kaspa-txscript = { git = "https://github.com/kaspanet/rusty-kaspa", rev = "98a4ccd8d200853787f227bd4536ac540cf34957" }
+secp256k1 = { version = "0.29", features = ["global-context", "rand-std"] }
+blake2b_simd = "1"
+hex = "0.4"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+[profile.release]
+lto = "thin"
diff --git a/vendor/kascov-preflight/LICENSE b/vendor/kascov-preflight/LICENSE
new file mode 100644
index 0000000..a2f7cda
--- /dev/null
+++ b/vendor/kascov-preflight/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Michiel Hamblok
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/kascov-preflight/README.md b/vendor/kascov-preflight/README.md
new file mode 100644
index 0000000..16a8477
--- /dev/null
+++ b/vendor/kascov-preflight/README.md
@@ -0,0 +1,20 @@
+# Vendored Kascov preflight snapshot
+
+This workspace contains the minimum MIT-licensed Kascov source required to
+build Studio's offline transaction preflight helper.
+
+- Upstream repository: `https://github.com/Knitser/kascov`
+- Upstream commit: `b64d6b4114df324f899783080371f26b619b19d0`
+- Pinned rusty-kaspa commit: `98a4ccd8d200853787f227bd4536ac540cf34957`
+- License: see `LICENSE`
+
+The upstream repository was replaced on 2026-08-09 and the pinned commit was
+no longer fetchable by a clean CI checkout. Studio therefore commits the
+audited preflight module, its two required Kascov crates, fixtures, and the
+Cargo lockfile. Runtime preflight remains pure local computation and does not
+contact Kascov or a Kaspa node.
+
+Studio's wrapper changes only the `Network` import, provides the stdin/stdout
+CLI entry point, and gates the upstream database fixture refresher behind the
+disabled `kascov-index-fixture` feature. The release binary does not include
+that index-only fixture tool.
diff --git a/vendor/kascov-preflight/crates/kascov-decode/Cargo.toml b/vendor/kascov-preflight/crates/kascov-decode/Cargo.toml
new file mode 100644
index 0000000..218b34d
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-decode/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "kascov-decode"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[dependencies]
+hex = { workspace = true }
+serde = { workspace = true }
+blake2b_simd = { workspace = true }
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v1_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v1_a.bin
new file mode 100644
index 0000000..cde9b8e
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v1_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v1_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v1_b.bin
new file mode 100644
index 0000000..0460631
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v1_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v2_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v2_a.bin
new file mode 100644
index 0000000..e21402e
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v2_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v2_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v2_b.bin
new file mode 100644
index 0000000..0b17bb6
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_buy_v2_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_col_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_col_a.bin
new file mode 100644
index 0000000..9b9a1ca
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_col_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_col_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_col_b.bin
new file mode 100644
index 0000000..bfaa7ef
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_col_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v1_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v1_a.bin
new file mode 100644
index 0000000..c53a30a
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v1_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v1_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v1_b.bin
new file mode 100644
index 0000000..0797ce3
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v1_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v2_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v2_a.bin
new file mode 100644
index 0000000..1805432
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v2_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v2_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v2_b.bin
new file mode 100644
index 0000000..a2a2b47
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/g0_list_v2_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_a_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_a_a.bin
new file mode 100644
index 0000000..4b4f86e
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_a_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_a_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_a_b.bin
new file mode 100644
index 0000000..acc03e1
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_a_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_b_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_b_a.bin
new file mode 100644
index 0000000..596a527
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_b_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_b_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_b_b.bin
new file mode 100644
index 0000000..5b5742a
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_b_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_c_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_c_a.bin
new file mode 100644
index 0000000..5affa59
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_c_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_c_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_c_b.bin
new file mode 100644
index 0000000..4b0ae49
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_c_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_minter_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_minter_a.bin
new file mode 100644
index 0000000..34aabf9
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_minter_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_minter_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_minter_b.bin
new file mode 100644
index 0000000..7cba823
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/kcc20_minter_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/pure_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/pure_a.bin
new file mode 100644
index 0000000..63aa723
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/pure_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/pure_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/pure_b.bin
new file mode 100644
index 0000000..1346f30
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/pure_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_di4m_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_di4m_a.bin
new file mode 100644
index 0000000..16ddec1
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_di4m_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_di4m_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_di4m_b.bin
new file mode 100644
index 0000000..653dcdd
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_di4m_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_gz4m_a.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_gz4m_a.bin
new file mode 100644
index 0000000..864f599
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_gz4m_a.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_gz4m_b.bin b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_gz4m_b.bin
new file mode 100644
index 0000000..c86fccc
Binary files /dev/null and b/vendor/kascov-preflight/crates/kascov-decode/fixtures/slot_mint_gz4m_b.bin differ
diff --git a/vendor/kascov-preflight/crates/kascov-decode/src/disasm.rs b/vendor/kascov-preflight/crates/kascov-decode/src/disasm.rs
new file mode 100644
index 0000000..7bcb72b
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-decode/src/disasm.rs
@@ -0,0 +1,268 @@
+//! Kaspa Script disassembler, covering the post-Toccata opcode set
+//! (KIP-17 introspection + covenant + ZK opcodes included).
+//!
+//! Opcode table extracted from rusty-kaspa `crypto/txscript/src/opcodes/mod.rs`
+//! at rev 98a4ccd (the workspace's pinned dependency rev).
+
+use std::fmt;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum OpGroup {
+ /// Data pushes
+ Push,
+ /// Pre-Toccata standard opcodes
+ Standard,
+ /// KIP-17 transaction introspection (0xb2–0xc9, 0xcd, 0xce)
+ Introspection,
+ /// KIP-20 covenant opcodes (0xcb, 0xcc, 0xcf–0xd6)
+ Covenant,
+ /// KIP-16 ZK verification (OpZkPrecompile)
+ Zk,
+ Unknown,
+}
+
+#[derive(Clone, Debug, serde::Serialize)]
+pub struct Instruction {
+ pub offset: usize,
+ pub opcode: u8,
+ pub name: &'static str,
+ pub group: OpGroup,
+ /// Pushed data, for push instructions.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub data: Option>,
+}
+
+impl fmt::Display for Instruction {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match &self.data {
+ Some(data) if !data.is_empty() => write!(f, "{} 0x{}", self.name, hex::encode(data)),
+ _ => f.write_str(self.name),
+ }
+ }
+}
+
+/// Disassemble a script. Returns instructions plus a flag for truncated /
+/// malformed tails (a push running past the end).
+pub fn disassemble(script: &[u8]) -> (Vec, bool) {
+ let mut out = Vec::new();
+ let mut i = 0usize;
+ while i < script.len() {
+ let offset = i;
+ let opcode = script[i];
+ i += 1;
+ let (name, group) = opcode_info(opcode);
+
+ let data_len = match opcode {
+ 0x01..=0x4b => Some(opcode as usize),
+ 0x4c => script.get(i).map(|&n| {
+ i += 1;
+ n as usize
+ }),
+ 0x4d => script.get(i..i + 2).map(|b| {
+ i += 2;
+ u16::from_le_bytes([b[0], b[1]]) as usize
+ }),
+ 0x4e => script.get(i..i + 4).map(|b| {
+ i += 4;
+ u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize
+ }),
+ _ => None,
+ };
+
+ match data_len {
+ None => out.push(Instruction { offset, opcode, name, group, data: None }),
+ Some(len) => {
+ let Some(data) = script.get(i..i + len) else {
+ out.push(Instruction { offset, opcode, name, group, data: None });
+ return (out, true);
+ };
+ i += len;
+ out.push(Instruction { offset, opcode, name, group, data: Some(data.to_vec()) });
+ }
+ }
+ }
+ (out, false)
+}
+
+pub fn opcode_info(opcode: u8) -> (&'static str, OpGroup) {
+ use OpGroup::*;
+ match opcode {
+ 0x00 => ("OpFalse", Push),
+ 0x01..=0x4b => ("OpData", Push),
+ 0x4c => ("OpPushData1", Push),
+ 0x4d => ("OpPushData2", Push),
+ 0x4e => ("OpPushData4", Push),
+ 0x4f => ("Op1Negate", Push),
+ 0x50 => ("OpReserved", Standard),
+ 0x51 => ("OpTrue", Push),
+ 0x52 => ("Op2", Push),
+ 0x53 => ("Op3", Push),
+ 0x54 => ("Op4", Push),
+ 0x55 => ("Op5", Push),
+ 0x56 => ("Op6", Push),
+ 0x57 => ("Op7", Push),
+ 0x58 => ("Op8", Push),
+ 0x59 => ("Op9", Push),
+ 0x5a => ("Op10", Push),
+ 0x5b => ("Op11", Push),
+ 0x5c => ("Op12", Push),
+ 0x5d => ("Op13", Push),
+ 0x5e => ("Op14", Push),
+ 0x5f => ("Op15", Push),
+ 0x60 => ("Op16", Push),
+ 0x61 => ("OpNop", Standard),
+ 0x62 => ("OpVer", Standard),
+ 0x63 => ("OpIf", Standard),
+ 0x64 => ("OpNotIf", Standard),
+ 0x65 => ("OpVerIf", Standard),
+ 0x66 => ("OpVerNotIf", Standard),
+ 0x67 => ("OpElse", Standard),
+ 0x68 => ("OpEndIf", Standard),
+ 0x69 => ("OpVerify", Standard),
+ 0x6a => ("OpReturn", Standard),
+ 0x6b => ("OpToAltStack", Standard),
+ 0x6c => ("OpFromAltStack", Standard),
+ 0x6d => ("Op2Drop", Standard),
+ 0x6e => ("Op2Dup", Standard),
+ 0x6f => ("Op3Dup", Standard),
+ 0x70 => ("Op2Over", Standard),
+ 0x71 => ("Op2Rot", Standard),
+ 0x72 => ("Op2Swap", Standard),
+ 0x73 => ("OpIfDup", Standard),
+ 0x74 => ("OpDepth", Standard),
+ 0x75 => ("OpDrop", Standard),
+ 0x76 => ("OpDup", Standard),
+ 0x77 => ("OpNip", Standard),
+ 0x78 => ("OpOver", Standard),
+ 0x79 => ("OpPick", Standard),
+ 0x7a => ("OpRoll", Standard),
+ 0x7b => ("OpRot", Standard),
+ 0x7c => ("OpSwap", Standard),
+ 0x7d => ("OpTuck", Standard),
+ 0x7e => ("OpCat", Standard),
+ 0x7f => ("OpSubstr", Standard),
+ 0x80 => ("OpLeft", Standard),
+ 0x81 => ("OpRight", Standard),
+ 0x82 => ("OpSize", Standard),
+ 0x83 => ("OpInvert", Standard),
+ 0x84 => ("OpAnd", Standard),
+ 0x85 => ("OpOr", Standard),
+ 0x86 => ("OpXor", Standard),
+ 0x87 => ("OpEqual", Standard),
+ 0x88 => ("OpEqualVerify", Standard),
+ 0x89 => ("OpReserved1", Standard),
+ 0x8a => ("OpReserved2", Standard),
+ 0x8b => ("Op1Add", Standard),
+ 0x8c => ("Op1Sub", Standard),
+ 0x8d => ("Op2Mul", Standard),
+ 0x8e => ("Op2Div", Standard),
+ 0x8f => ("OpNegate", Standard),
+ 0x90 => ("OpAbs", Standard),
+ 0x91 => ("OpNot", Standard),
+ 0x92 => ("Op0NotEqual", Standard),
+ 0x93 => ("OpAdd", Standard),
+ 0x94 => ("OpSub", Standard),
+ 0x95 => ("OpMul", Standard),
+ 0x96 => ("OpDiv", Standard),
+ 0x97 => ("OpMod", Standard),
+ 0x98 => ("OpLShift", Standard),
+ 0x99 => ("OpRShift", Standard),
+ 0x9a => ("OpBoolAnd", Standard),
+ 0x9b => ("OpBoolOr", Standard),
+ 0x9c => ("OpNumEqual", Standard),
+ 0x9d => ("OpNumEqualVerify", Standard),
+ 0x9e => ("OpNumNotEqual", Standard),
+ 0x9f => ("OpLessThan", Standard),
+ 0xa0 => ("OpGreaterThan", Standard),
+ 0xa1 => ("OpLessThanOrEqual", Standard),
+ 0xa2 => ("OpGreaterThanOrEqual", Standard),
+ 0xa3 => ("OpMin", Standard),
+ 0xa4 => ("OpMax", Standard),
+ 0xa5 => ("OpWithin", Standard),
+ 0xa6 => ("OpZkPrecompile", Zk),
+ 0xa7 => ("OpBlake2bWithKey", Standard),
+ 0xa8 => ("OpSHA256", Standard),
+ 0xa9 => ("OpCheckMultiSigECDSA", Standard),
+ 0xaa => ("OpBlake2b", Standard),
+ 0xab => ("OpCheckSigECDSA", Standard),
+ 0xac => ("OpCheckSig", Standard),
+ 0xad => ("OpCheckSigVerify", Standard),
+ 0xae => ("OpCheckMultiSig", Standard),
+ 0xaf => ("OpCheckMultiSigVerify", Standard),
+ 0xb0 => ("OpCheckLockTimeVerify", Standard),
+ 0xb1 => ("OpCheckSequenceVerify", Standard),
+ 0xb2 => ("OpTxVersion", Introspection),
+ 0xb3 => ("OpTxInputCount", Introspection),
+ 0xb4 => ("OpTxOutputCount", Introspection),
+ 0xb5 => ("OpTxLockTime", Introspection),
+ 0xb6 => ("OpTxSubnetId", Introspection),
+ 0xb7 => ("OpTxGas", Introspection),
+ 0xb8 => ("OpTxPayloadSubstr", Introspection),
+ 0xb9 => ("OpTxInputIndex", Introspection),
+ 0xba => ("OpOutpointTxId", Introspection),
+ 0xbb => ("OpOutpointIndex", Introspection),
+ 0xbc => ("OpTxInputScriptSigSubstr", Introspection),
+ 0xbd => ("OpTxInputSeq", Introspection),
+ 0xbe => ("OpTxInputAmount", Introspection),
+ 0xbf => ("OpTxInputSpk", Introspection),
+ 0xc0 => ("OpTxInputDaaScore", Introspection),
+ 0xc1 => ("OpTxInputIsCoinbase", Introspection),
+ 0xc2 => ("OpTxOutputAmount", Introspection),
+ 0xc3 => ("OpTxOutputSpk", Introspection),
+ 0xc4 => ("OpTxPayloadLen", Introspection),
+ 0xc5 => ("OpTxInputSpkLen", Introspection),
+ 0xc6 => ("OpTxInputSpkSubstr", Introspection),
+ 0xc7 => ("OpTxOutputSpkLen", Introspection),
+ 0xc8 => ("OpTxOutputSpkSubstr", Introspection),
+ 0xc9 => ("OpTxInputScriptSigLen", Introspection),
+ 0xcb => ("OpAuthOutputCount", Covenant),
+ 0xcc => ("OpAuthOutputIdx", Covenant),
+ 0xcd => ("OpNum2Bin", Introspection),
+ 0xce => ("OpBin2Num", Introspection),
+ 0xcf => ("OpInputCovenantId", Covenant),
+ 0xd0 => ("OpCovInputCount", Covenant),
+ 0xd1 => ("OpCovInputIdx", Covenant),
+ 0xd2 => ("OpCovOutputCount", Covenant),
+ 0xd3 => ("OpCovOutputIdx", Covenant),
+ 0xd4 => ("OpChainblockSeqCommit", Covenant),
+ 0xd5 => ("OpOutputCovenantId", Covenant),
+ 0xd6 => ("OpOutputAuthorizingInput", Covenant),
+ 0xd7 => ("OpCheckSigFromStack", Standard),
+ 0xd8 => ("OpCheckSigFromStackECDSA", Standard),
+ 0xd9 => ("OpBlake3", Standard),
+ 0xda => ("OpBlake3WithKey", Standard),
+ 0xfa => ("OpSmallInteger", Standard),
+ 0xfb => ("OpPubKeys", Standard),
+ 0xfd => ("OpPubKeyHash", Standard),
+ 0xfe => ("OpPubKey", Standard),
+ 0xff => ("OpInvalidOpCode", Standard),
+ _ => ("OpUnknown", Unknown),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn disassembles_covenant_style_script() {
+ // OpTxInputIndex OpInputCovenantId OpData32 <32B> OpEqualVerify OpTrue
+ let mut script = vec![0xb9, 0xcf, 0x20];
+ script.extend([0x11; 32]);
+ script.extend([0x88, 0x51]);
+ let (instructions, truncated) = disassemble(&script);
+ assert!(!truncated);
+ let names: Vec<_> = instructions.iter().map(|i| i.name).collect();
+ assert_eq!(names, ["OpTxInputIndex", "OpInputCovenantId", "OpData", "OpEqualVerify", "OpTrue"]);
+ assert_eq!(instructions[1].group, OpGroup::Covenant);
+ assert_eq!(instructions[2].data.as_deref(), Some([0x11; 32].as_slice()));
+ }
+
+ #[test]
+ fn flags_truncated_push() {
+ let (instructions, truncated) = disassemble(&[0x4c, 0x20, 0x01]);
+ assert!(truncated);
+ assert_eq!(instructions.len(), 1);
+ }
+}
diff --git a/vendor/kascov-preflight/crates/kascov-decode/src/kcc1.rs b/vendor/kascov-preflight/crates/kascov-decode/src/kcc1.rs
new file mode 100644
index 0000000..da4c5eb
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-decode/src/kcc1.rs
@@ -0,0 +1,544 @@
+//! KCC-0001 conformance primitives — byte layouts and hash derivations from
+//! "Covenant definition, concepts, bytes layout and ABI" (IzioDev/kccs,
+//! commit 55b28d8, Draft). Section numbers in doc comments refer to that
+//! spec. Invocation arguments use PushMinimal, which is `encode_push` in the
+//! crate root; only the KCC1-specific encodings live here.
+
+use crate::encode_push;
+
+/// Unkeyed BLAKE2b with 32-byte output — the spec's `Hash` (§3.1).
+fn hash32(input: &[u8]) -> [u8; 32] {
+ let mut out = [0u8; 32];
+ out.copy_from_slice(blake2b_simd::Params::new().hash_length(32).hash(input).as_bytes());
+ out
+}
+
+/// `PushExplicit(b)` (§5.2): `OP_0` for the empty payload; the length-based
+/// forms (`OP_DATA_n` / `OP_PUSHDATA1/2/4`) for every non-empty payload —
+/// never the numeric opcodes `OP_1..OP_16` / `OP_1NEGATE`.
+pub fn push_explicit(payload: &[u8]) -> Vec {
+ match payload.len() {
+ 0 => vec![0x00],
+ n @ 1..=75 => {
+ let mut out = Vec::with_capacity(n + 1);
+ out.push(n as u8);
+ out.extend_from_slice(payload);
+ out
+ }
+ n @ 76..=0xff => {
+ let mut out = vec![0x4c, n as u8];
+ out.extend_from_slice(payload);
+ out
+ }
+ n @ 0x100..=0xffff => {
+ let mut out = vec![0x4d, (n & 0xff) as u8, (n >> 8) as u8];
+ out.extend_from_slice(payload);
+ out
+ }
+ n => {
+ let n = n as u32;
+ let mut out = vec![0x4e, (n & 0xff) as u8, (n >> 8 & 0xff) as u8, (n >> 16 & 0xff) as u8, (n >> 24) as u8];
+ out.extend_from_slice(payload);
+ out
+ }
+ }
+}
+
+/// Decode exactly one `PushExplicit` at the start of `script` (§5.2), giving
+/// the payload and the bytes consumed. §8.1 requires the consumed bytes to
+/// equal `PushExplicit(payload)` byte-for-byte, so numeric opcodes,
+/// non-canonical length forms (`OP_PUSHDATA1` over a payload that fits
+/// `OP_DATA_n`, …), and truncated pushes are all `None`.
+pub fn read_push_explicit(script: &[u8]) -> Option<(&[u8], usize)> {
+ let (&op, rest) = script.split_first()?;
+ let (len, header) = match op {
+ 0x00 => return Some((&[], 1)),
+ 1..=75 => (op as usize, 1),
+ 0x4c => {
+ let n = *rest.first()? as usize;
+ if n < 76 {
+ return None;
+ }
+ (n, 2)
+ }
+ 0x4d => {
+ let n = u16::from_le_bytes(rest.get(..2)?.try_into().ok()?) as usize;
+ if n < 0x100 {
+ return None;
+ }
+ (n, 3)
+ }
+ 0x4e => {
+ let n = u32::from_le_bytes(rest.get(..4)?.try_into().ok()?) as usize;
+ if n < 0x10000 {
+ return None;
+ }
+ (n, 5)
+ }
+ _ => return None,
+ };
+ let payload = script.get(header..header + len)?;
+ Some((payload, header + len))
+}
+
+/// Eight-byte little-endian signed-magnitude `int` state payload (§5.3/§5.4):
+/// magnitude in the low 63 bits, sign in the top bit of the last byte.
+/// `None` for `i64::MIN` — the §5.3 range is symmetric and its magnitude
+/// does not fit.
+pub fn encode_state_int(value: i64) -> Option<[u8; 8]> {
+ if value == i64::MIN {
+ return None;
+ }
+ let mut out = value.unsigned_abs().to_le_bytes();
+ if value < 0 {
+ out[7] |= 0x80;
+ }
+ Some(out)
+}
+
+/// Inverse of `encode_state_int`. A set sign bit over a zero magnitude is
+/// `None`: no in-range value encodes to it, and accepting it would give zero
+/// two encodings, breaking §8.1's byte-exactness requirement.
+pub fn decode_state_int(bytes: &[u8; 8]) -> Option {
+ let mut magnitude = *bytes;
+ magnitude[7] &= 0x7f;
+ let magnitude = u64::from_le_bytes(magnitude) as i64;
+ match (bytes[7] & 0x80 != 0, magnitude) {
+ (false, m) => Some(m),
+ (true, 0) => None,
+ (true, m) => Some(-m),
+ }
+}
+
+/// Minimal ScriptNum for an `int` invocation argument (§5.3): little-endian
+/// magnitude, sign carried by the top bit of the last byte, one extension
+/// byte only when the magnitude's own top bit is set. The crate root's
+/// `snum` is documented non-negative-only; this codec also covers negative
+/// values. `None` for `i64::MIN` (out of the §5.3 range).
+pub fn encode_arg_int(value: i64) -> Option> {
+ if value == i64::MIN {
+ return None;
+ }
+ let mut magnitude = value.unsigned_abs();
+ let mut out = Vec::new();
+ while magnitude > 0 {
+ out.push((magnitude & 0xff) as u8);
+ magnitude >>= 8;
+ }
+ if out.last().is_some_and(|b| b & 0x80 != 0) {
+ out.push(0);
+ }
+ if value < 0 {
+ // value < 0 implies a non-empty magnitude
+ let last = out.len() - 1;
+ out[last] |= 0x80;
+ }
+ Some(out)
+}
+
+/// Inverse of `encode_arg_int`: `None` unless `bytes` is the minimal
+/// ScriptNum of a value in the §5.3 range — padded encodings, negative
+/// zero, and out-of-range magnitudes are all rejected.
+pub fn decode_arg_int(bytes: &[u8]) -> Option {
+ let Some((&last, head)) = bytes.split_last() else {
+ return Some(0);
+ };
+ // minimality: a last byte carrying only the sign bit must be shielding
+ // the previous byte's high bit
+ if last & 0x7f == 0 && !head.last().is_some_and(|b| b & 0x80 != 0) {
+ return None;
+ }
+ // an in-range value needs at most nine bytes even with an extension byte
+ if bytes.len() > 9 {
+ return None;
+ }
+ let mut magnitude = ((last & 0x7f) as u128) << (8 * head.len());
+ for (i, &b) in head.iter().enumerate() {
+ magnitude |= (b as u128) << (8 * i);
+ }
+ let magnitude = i64::try_from(magnitude).ok()?;
+ Some(if last & 0x80 != 0 { -magnitude } else { magnitude })
+}
+
+/// Dispatch tag (§6.1): the first four bytes of `Hash(UTF8(signature))`,
+/// where `signature` is `"{name}({comma-separated canonical type names})"`
+/// with no whitespace — e.g. `"step(int,byte[4],bool,byte)"`.
+pub fn dispatch_tag(signature: &str) -> [u8; 4] {
+ let mut tag = [0u8; 4];
+ tag.copy_from_slice(&hash32(signature.as_bytes())[..4]);
+ tag
+}
+
+/// `TemplateHash(prefix, suffix)` (§8.3):
+/// `Hash(LE64(len(prefix)) || prefix || LE64(len(suffix)) || suffix)`.
+/// The length fields bind the prefix/suffix boundary — a plain
+/// `Hash(prefix || suffix)` would collide across different cuts.
+pub fn template_hash(prefix: &[u8], suffix: &[u8]) -> [u8; 32] {
+ let mut state = blake2b_simd::Params::new().hash_length(32).to_state();
+ state.update(&(prefix.len() as u64).to_le_bytes());
+ state.update(prefix);
+ state.update(&(suffix.len() as u64).to_le_bytes());
+ state.update(suffix);
+ let mut out = [0u8; 32];
+ out.copy_from_slice(state.finalize().as_bytes());
+ out
+}
+
+/// Version-0 P2SH script public key committing to `program` (§7):
+/// `OP_BLAKE2B OP_DATA_32 Hash(R) OP_EQUAL`.
+pub fn envelope_spk(program: &[u8]) -> Vec {
+ let mut out = Vec::with_capacity(35);
+ out.push(0xaa);
+ out.push(0x20);
+ out.extend_from_slice(&hash32(program));
+ out.push(0x87);
+ out
+}
+
+/// Signature script spending a KCC1 P2SH output (§7): the pre-encoded
+/// argument pushes, then `OP_DATA_4 dispatch_tag` (present iff the program
+/// declares two or more invocable branches — pass `None` for exactly one),
+/// then `PushMinimal(R)` as the mandatory final push.
+pub fn signature_script(arg_pushes: &[u8], dispatch: Option<&[u8; 4]>, program: &[u8]) -> Vec {
+ let mut out = arg_pushes.to_vec();
+ if let Some(tag) = dispatch {
+ out.push(0x04);
+ out.extend_from_slice(tag);
+ }
+ out.extend_from_slice(&encode_push(program));
+ out
+}
+
+/// Scalar field types with a defined state lowering (§5.1/§5.4). Arrays and
+/// records lower to sequences of these before encoding (§5.5/§5.6).
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum FieldType {
+ Int,
+ Bool,
+ Byte,
+ /// Variable byte string (`bytes`); no fixed width, so invalid in packed
+ /// virtual-element payloads (§10.1).
+ Bytes,
+ /// UTF-8 string, no terminator (`string`); variable width like `Bytes`.
+ String,
+ /// 32-byte public key (`pubkey`).
+ PubKey,
+ /// 65-byte transaction signature (`sig`).
+ Sig,
+ /// 64-byte data signature (`datasig`).
+ DataSig,
+ /// `byte[N]`.
+ FixedBytes(usize),
+}
+
+/// A field value; paired with a `FieldType` when encoding or decoding.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum StateValue {
+ Int(i64),
+ Bool(bool),
+ /// Every bytes-like type; width and content checked against the type.
+ Bytes(Vec),
+}
+
+/// Canonical state payload of one lowered field (§5.3/§5.4). `None` when the
+/// value does not fit the type: wrong variant, wrong width, invalid UTF-8,
+/// or an out-of-range int.
+pub fn state_payload(ty: FieldType, value: &StateValue) -> Option> {
+ match (ty, value) {
+ (FieldType::Int, StateValue::Int(v)) => Some(encode_state_int(*v)?.to_vec()),
+ (FieldType::Bool, StateValue::Bool(b)) => Some(vec![*b as u8]),
+ (_, StateValue::Bytes(b)) => {
+ let ok = match ty {
+ FieldType::Byte => b.len() == 1,
+ FieldType::Bytes => true,
+ FieldType::String => std::str::from_utf8(b).is_ok(),
+ FieldType::PubKey => b.len() == 32,
+ FieldType::Sig => b.len() == 65,
+ FieldType::DataSig => b.len() == 64,
+ FieldType::FixedBytes(n) => b.len() == n,
+ FieldType::Int | FieldType::Bool => false,
+ };
+ ok.then(|| b.clone())
+ }
+ _ => None,
+ }
+}
+
+/// Encode ordered state fields (§8.1): each field's canonical payload
+/// wrapped in `PushExplicit`, concatenated in declaration order.
+pub fn encode_state(fields: &[(FieldType, StateValue)]) -> Option> {
+ let mut out = Vec::new();
+ for (ty, value) in fields {
+ out.extend_from_slice(&push_explicit(&state_payload(*ty, value)?));
+ }
+ Some(out)
+}
+
+/// Decode an encoded state block against its declared field types (§8.1):
+/// exactly one canonical `PushExplicit` per field, each payload validated
+/// per type, trailing bytes rejected.
+pub fn decode_state(types: &[FieldType], encoded: &[u8]) -> Option> {
+ let mut at = 0;
+ let mut values = Vec::with_capacity(types.len());
+ for &ty in types {
+ let (payload, consumed) = read_push_explicit(&encoded[at..])?;
+ values.push(decode_payload(ty, payload)?);
+ at += consumed;
+ }
+ (at == encoded.len()).then_some(values)
+}
+
+fn decode_payload(ty: FieldType, payload: &[u8]) -> Option {
+ match ty {
+ FieldType::Int => Some(StateValue::Int(decode_state_int(payload.try_into().ok()?)?)),
+ FieldType::Bool => match payload {
+ [0x00] => Some(StateValue::Bool(false)),
+ [0x01] => Some(StateValue::Bool(true)),
+ _ => None,
+ },
+ _ => {
+ // width and content rules are the encoder's, re-checked in reverse
+ let value = StateValue::Bytes(payload.to_vec());
+ state_payload(ty, &value).map(|_| value)
+ }
+ }
+}
+
+/// `Packed(value)` (§10.1): the fields' fixed payloads concatenated without
+/// push opcodes. Defined only for layouts with a statically known packed
+/// width, so the variable-width `bytes`/`string` types are `None`.
+pub fn packed(fields: &[(FieldType, StateValue)]) -> Option> {
+ let mut out = Vec::new();
+ for (ty, value) in fields {
+ if matches!(ty, FieldType::Bytes | FieldType::String) {
+ return None;
+ }
+ out.extend_from_slice(&state_payload(*ty, value)?);
+ }
+ Some(out)
+}
+
+/// Hash-committed virtual element (§10.1): `commitment = Hash(Packed(value))`,
+/// stored in state as a `byte[32]` field.
+pub fn commitment(payload: &[u8]) -> [u8; 32] {
+ hash32(payload)
+}
+
+/// Verify an opening against a `byte[32]` commitment field (§10.1):
+/// `Hash(payload) = commitment`. The opening is witness data, not encoded
+/// state, and MUST be verified before the value is used.
+pub fn verify_commitment(commitment: &[u8], payload: &[u8]) -> bool {
+ commitment == hash32(payload).as_slice()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn h(s: &str) -> Vec {
+ hex::decode(s).unwrap()
+ }
+
+ const STEP_SIGNATURE: &str = "step(int,byte[4],bool,byte)";
+
+ // §5.2 worked contrast: PushMinimal(01) = OP_1, PushExplicit(01) = OP_DATA_1 01.
+ #[test]
+ fn push_explicit_never_uses_numeric_opcodes() {
+ assert_eq!(encode_push(&[0x01]), vec![0x51]);
+ assert_eq!(push_explicit(&[0x01]), vec![0x01, 0x01]);
+ assert_eq!(push_explicit(&[]), vec![0x00]); // OP_0 is still the empty push
+ assert_eq!(push_explicit(&[0x81]), vec![0x01, 0x81]); // not OP_1NEGATE
+ assert_eq!(push_explicit(&vec![0xee; 76])[..2], [0x4c, 76]);
+ assert_eq!(push_explicit(&vec![0xee; 0x100])[..3], [0x4d, 0x00, 0x01]);
+ }
+
+ #[test]
+ fn read_push_explicit_round_trips_and_rejects_non_canonical() {
+ for payload in [vec![], vec![0x01], vec![0x81], vec![0x07; 75], vec![0x07; 76], vec![0x07; 0x100]] {
+ let encoded = push_explicit(&payload);
+ assert_eq!(read_push_explicit(&encoded), Some((payload.as_slice(), encoded.len())));
+ }
+ assert_eq!(read_push_explicit(&[0x51]), None); // OP_1
+ assert_eq!(read_push_explicit(&[0x4f]), None); // OP_1NEGATE
+ assert_eq!(read_push_explicit(&[0x4c, 0x01, 0xaa]), None); // PUSHDATA1 over a 1-byte payload
+ assert_eq!(read_push_explicit(&[0x4d, 0x01, 0x00, 0xaa]), None); // PUSHDATA2 under 256
+ assert_eq!(read_push_explicit(&[0x02, 0xaa]), None); // truncated
+ assert_eq!(read_push_explicit(&[]), None);
+ }
+
+ // §11.1
+ #[test]
+ fn vector_11_1_dispatch_tag() {
+ assert_eq!(dispatch_tag(STEP_SIGNATURE), [0x3a, 0x08, 0x8d, 0x13]);
+ }
+
+ // §11.1 — arguments (17, 01020304, true, 01) plus the dispatch-tag push.
+ #[test]
+ fn vector_11_1_argument_encoding() {
+ let mut combined = Vec::new();
+ combined.extend(encode_push(&encode_arg_int(17).unwrap())); // int 17 = 0111
+ combined.extend(encode_push(&h("01020304"))); // byte[4]
+ combined.push(0x51); // standalone bool true = OP_1 (§5.4)
+ combined.extend(encode_push(&[0x01])); // byte 01 = OP_1
+ combined.push(0x04); // OP_DATA_4 dispatch tag (§7)
+ combined.extend(dispatch_tag(STEP_SIGNATURE));
+ assert_eq!(hex::encode(&combined), "011104010203045151043a088d13");
+ }
+
+ // §11.2 — one-byte program R = 51.
+ #[test]
+ fn vector_11_2_p2sh_envelope() {
+ let program = [0x51];
+ assert_eq!(
+ hex::encode(envelope_spk(&program)),
+ "aa20ce57216285125006ec18197bd8184221cefa559bb0798410d99a5bba5b07cd1d87"
+ );
+ assert_eq!(hex::encode(signature_script(&[], None, &program)), "0151");
+ // §11.1 arguments + tag ahead of the mandatory final PushMinimal(R)
+ let args = h("011104010203045151");
+ assert_eq!(
+ hex::encode(signature_script(&args, Some(&dispatch_tag(STEP_SIGNATURE)), &program)),
+ "011104010203045151043a088d130151"
+ );
+ }
+
+ // §11.3 — pubkey 07^32, int -5, bool true.
+ const STATE_11_3: &str =
+ "2007070707070707070707070707070707070707070707070707070707070707070805000000000000800101";
+ const TYPES_11_3: [FieldType; 3] = [FieldType::PubKey, FieldType::Int, FieldType::Bool];
+
+ #[test]
+ fn vector_11_3_state_encoding() {
+ assert_eq!(hex::encode(encode_state_int(-5).unwrap()), "0500000000000080");
+ let fields = [
+ (FieldType::PubKey, StateValue::Bytes(vec![0x07; 32])),
+ (FieldType::Int, StateValue::Int(-5)),
+ (FieldType::Bool, StateValue::Bool(true)),
+ ];
+ assert_eq!(hex::encode(encode_state(&fields).unwrap()), STATE_11_3);
+ }
+
+ #[test]
+ fn vector_11_3_state_decoding() {
+ let encoded = h(STATE_11_3);
+ assert_eq!(
+ decode_state(&TYPES_11_3, &encoded),
+ Some(vec![
+ StateValue::Bytes(vec![0x07; 32]),
+ StateValue::Int(-5),
+ StateValue::Bool(true),
+ ])
+ );
+ // §8.1 rejections: trailing bytes, missing fields, wrong widths
+ let mut trailing = encoded.clone();
+ trailing.push(0x00);
+ assert_eq!(decode_state(&TYPES_11_3, &trailing), None);
+ assert_eq!(decode_state(&TYPES_11_3, &encoded[..encoded.len() - 2]), None);
+ assert_eq!(decode_state(&[FieldType::Sig, FieldType::Int, FieldType::Bool], &encoded), None);
+ }
+
+ // §11.4
+ #[test]
+ fn vector_11_4_template_hashes() {
+ let rows: &[(&str, &str, &str)] = &[
+ ("", "", "94c1c088cc9453996779630ad3af45cbd92814828dd784cf2aa12df95d1b8afe"),
+ ("61", "6263", "77bbcab7072b897c548327378f11776f4853104c71bdb95a12ded5d2783523bf"),
+ ("6162", "63", "20263e794775e4edf2b306c0f306af9e50175c831c857604b481e847f790bf95"),
+ ("00ff", "100080", "81485678b557bcd4a836c2db54ee268e1dc08549f1b8e4d8d67960321b765f25"),
+ ];
+ for (prefix, suffix, want) in rows {
+ assert_eq!(
+ hex::encode(template_hash(&h(prefix), &h(suffix))),
+ *want,
+ "prefix={prefix} suffix={suffix}"
+ );
+ }
+ // rows 2 and 3 concatenate identically; the LE64 length fields split them
+ assert_ne!(template_hash(&h("61"), &h("6263")), template_hash(&h("6162"), &h("63")));
+ }
+
+ // §11.5 — R = 5102aabb010102ccdd75, state.start = 1, state.len = 8.
+ #[test]
+ fn vector_11_5_template_views() {
+ let r = h("5102aabb010102ccdd75");
+ // fields byte[2] a = aabb, bool b = true, byte[2] c = ccdd
+ assert_eq!(
+ decode_state(
+ &[FieldType::FixedBytes(2), FieldType::Bool, FieldType::FixedBytes(2)],
+ &r[1..9]
+ ),
+ Some(vec![
+ StateValue::Bytes(h("aabb")),
+ StateValue::Bool(true),
+ StateValue::Bytes(h("ccdd")),
+ ])
+ );
+ let views: &[(usize, usize, &str, &str, &str, &str)] = &[
+ // (view.start, view.len, prefix, encoded_state, suffix, hash)
+ (1, 5, "51", "02aabb0101", "02ccdd75", "c44ab750e981ea120b9341a4107aa589d40d47f7a6c0b4fcb644ab344f893cfa"),
+ (4, 5, "5102aabb", "010102ccdd", "75", "7ba3a2319a0bbab234bef65c1198bf4b86edb778a8072762c5bb5ccdf7666ec4"),
+ (4, 2, "5102aabb", "0101", "02ccdd75", "82ea2f1d05005e6f6b4a2a29d3bf65315e11b4f85f7aa7d9a0c904ec03b6ab70"),
+ ];
+ for (start, len, want_prefix, want_state, want_suffix, want_hash) in views {
+ let (prefix, rest) = r.split_at(*start);
+ let (encoded_state, suffix) = rest.split_at(*len);
+ assert_eq!(hex::encode(prefix), *want_prefix);
+ assert_eq!(hex::encode(encoded_state), *want_state);
+ assert_eq!(hex::encode(suffix), *want_suffix);
+ assert_eq!(
+ hex::encode(template_hash(prefix, suffix)),
+ *want_hash,
+ "view [{start}, {})",
+ start + len
+ );
+ }
+ }
+
+ // §11.6 — fixed record (int -5, bool true).
+ #[test]
+ fn vector_11_6_hash_committed_virtual_element() {
+ let fields = [(FieldType::Int, StateValue::Int(-5)), (FieldType::Bool, StateValue::Bool(true))];
+ let payload = packed(&fields).unwrap();
+ assert_eq!(hex::encode(&payload), "050000000000008001");
+ let want = h("ce56c1a4ec3df391eb0692835e4529f8c5dd6da7c68e533ce68ba2f7dd35debf");
+ assert_eq!(commitment(&payload).as_slice(), want.as_slice());
+ assert!(verify_commitment(&want, &payload));
+ assert!(!verify_commitment(&want, &payload[..payload.len() - 1]));
+ assert!(!verify_commitment(&want[..31], &payload));
+ // Packed is undefined for variable-width layouts (§10.1)
+ assert_eq!(packed(&[(FieldType::Bytes, StateValue::Bytes(vec![0x01]))]), None);
+ }
+
+ #[test]
+ fn state_int_codec_edges() {
+ for v in [0i64, 1, -1, 127, -128, i64::MAX, -i64::MAX] {
+ assert_eq!(decode_state_int(&encode_state_int(v).unwrap()), Some(v), "{v}");
+ }
+ assert_eq!(hex::encode(encode_state_int(i64::MAX).unwrap()), "ffffffffffffff7f");
+ assert_eq!(hex::encode(encode_state_int(-i64::MAX).unwrap()), "ffffffffffffffff");
+ assert_eq!(encode_state_int(i64::MIN), None); // -2^63 is outside the §5.3 range
+ assert_eq!(decode_state_int(&[0, 0, 0, 0, 0, 0, 0, 0x80]), None); // negative zero
+ }
+
+ #[test]
+ fn arg_int_codec_is_minimal_and_signed() {
+ assert_eq!(encode_arg_int(17).unwrap(), vec![0x11]); // §11.1
+ assert_eq!(encode_arg_int(0).unwrap(), Vec::::new());
+ assert_eq!(encode_arg_int(-1).unwrap(), vec![0x81]);
+ assert_eq!(encode_arg_int(-5).unwrap(), vec![0x85]);
+ assert_eq!(encode_arg_int(128).unwrap(), vec![0x80, 0x00]);
+ assert_eq!(encode_arg_int(-128).unwrap(), vec![0x80, 0x80]);
+ assert_eq!(encode_arg_int(i64::MIN), None);
+ // matches the crate root's snum on its non-negative domain
+ for v in [0i64, 1, 6, 17, 127, 128, 32767, 100_000_000] {
+ assert_eq!(encode_arg_int(v).unwrap(), crate::snum(v));
+ }
+ for v in [0i64, 1, -1, 17, -5, 127, -127, 128, -128, 32767, -32768, i64::MAX, -i64::MAX] {
+ assert_eq!(decode_arg_int(&encode_arg_int(v).unwrap()), Some(v), "{v}");
+ }
+ assert_eq!(decode_arg_int(&[0x05, 0x00]), None); // padded
+ assert_eq!(decode_arg_int(&[0x00]), None); // padded zero
+ assert_eq!(decode_arg_int(&[0x80]), None); // negative zero
+ assert_eq!(decode_arg_int(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0x00]), None); // 2^63 is out of range
+ }
+}
diff --git a/vendor/kascov-preflight/crates/kascov-decode/src/kcc20.rs b/vendor/kascov-preflight/crates/kascov-decode/src/kcc20.rs
new file mode 100644
index 0000000..154a313
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-decode/src/kcc20.rs
@@ -0,0 +1,235 @@
+//! KCC20 state-level helpers: typed access to the "KCC20 token" state fields
+//! and the splice-and-hash primitive that proves an output's hidden state.
+//!
+//! Every registered KCC20 token build opens with the same alt-stack-guarded
+//! state block at fixed byte offsets:
+//!
+//! ```text
+//! 0x6b · 0x20 owner[2..34] · 0x01 type[35] · 0x08 amount[37..45] · 0x01 isMinter[46] · 0x6c
+//! ```
+//!
+//! verified across all 2,561 hash-verified TN10 reveals (state block ok=2561
+//! bad=0). Splicing a candidate state into a same-build program and checking
+//! blake2b-256(program) against a P2SH commitment is therefore a *proof* of
+//! that output's state — hash equality is the sole acceptance criterion, so a
+//! misparse can only fail closed, never accept a wrong state. Any future
+//! build with different offsets simply never passes the hash check.
+
+use crate::{p2sh_hash, Registry};
+
+/// Registry template name of the token contract (kcc20.sil).
+pub const TOKEN_TEMPLATE: &str = "KCC20 token";
+/// Registry template name of the two-token vault build ("minter" is the
+/// historical skeleton name; on TN10 these are stateless two-token vaults).
+pub const MINTER_TEMPLATE: &str = "KCC20 minter";
+
+/// One decoded KCC20 token state, raw field bytes preserved: hash proofs
+/// operate on exact bytes, and amount VALIDITY (script-number range) is a
+/// separate judgement from state IDENTITY.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct TokenState {
+ pub owner: [u8; 32],
+ pub identifier_type: u8,
+ /// The raw amount push (observed: always 8-byte little-endian).
+ pub amount_raw: Vec,
+ /// The raw isMinter push (observed: always 1 byte, 0x00 / 0x01).
+ pub minter_raw: Vec,
+}
+
+impl TokenState {
+ /// The amount as a non-negative i64, only for the canonical encoding the
+ /// chain uses: exactly 8 LE bytes with the script-number sign bit clear.
+ /// Anything else is out of model — callers must treat `None` as
+ /// unvalidatable, never coerce.
+ pub fn amount_i64(&self) -> Option {
+ let bytes: [u8; 8] = self.amount_raw.as_slice().try_into().ok()?;
+ let v = i64::from_le_bytes(bytes);
+ (v >= 0).then_some(v)
+ }
+
+ /// Strict boolean read of isMinter; `None` for any non-0x00/0x01 byte.
+ pub fn is_minter(&self) -> Option {
+ match self.minter_raw.as_slice() {
+ [0x00] => Some(false),
+ [0x01] => Some(true),
+ _ => None,
+ }
+ }
+
+ /// Owner key for aggregation: hex(identifier_type || owner_identifier).
+ pub fn owner_key(&self) -> String {
+ let mut bytes = Vec::with_capacity(33);
+ bytes.push(self.identifier_type);
+ bytes.extend_from_slice(&self.owner);
+ hex::encode(bytes)
+ }
+}
+
+/// Decode `program` as a KCC20 token state via the registry skeletons.
+/// Returns the four labeled fields only when the template is "KCC20 token"
+/// and every field is present with its observed width (owner 32 bytes,
+/// identifier_type 1 byte) — a partial or misshapen decode yields `None`.
+pub fn decode_token_state(registry: &Registry, spk_version: u16, program: &[u8]) -> Option {
+ let d = registry.decode(spk_version, program);
+ if d.template != Some(TOKEN_TEMPLATE) {
+ return None;
+ }
+ let field = |name: &str| d.fields.iter().find(|f| f.name == name).map(|f| f.value.clone());
+ let owner: [u8; 32] = field("owner_identifier")?.try_into().ok()?;
+ let id_type = field("identifier_type")?;
+ let [identifier_type] = id_type.as_slice() else { return None };
+ Some(TokenState {
+ owner,
+ identifier_type: *identifier_type,
+ amount_raw: field("amount")?,
+ minter_raw: field("is_minter")?,
+ })
+}
+
+/// Does `program` open with the fixed KCC20 state block (see module docs)?
+pub fn has_state_block(program: &[u8]) -> bool {
+ program.len() >= 48
+ && program[0] == 0x6b
+ && program[1] == 0x20
+ && program[34] == 0x01
+ && program[36] == 0x08
+ && program[45] == 0x01
+ && program[47] == 0x6c
+}
+
+/// KCC-1 draft §8.3 TemplateHash of a program carrying the verified KCC20
+/// state block: prefix is the leading alt-stack guard byte, the state range
+/// is bytes [1, 47), suffix is everything from the closing guard on. `None`
+/// when the block is absent — the canonical hash is only computed where the
+/// state range is proven, never guessed. Derivation pinned to spec commit
+/// 55b28d8; recompute is gated by the store's `kcc1_abi_version` meta.
+pub fn kcc1_template_hash(program: &[u8]) -> Option<[u8; 32]> {
+ has_state_block(program).then(|| crate::kcc1::template_hash(&program[..1], &program[47..]))
+}
+
+/// Splice a candidate state into a same-build program at the fixed state
+/// block. Returns `None` when the base program doesn't carry the block.
+/// The result is only meaningful after a hash check against a commitment.
+pub fn splice_token_state(
+ program: &[u8],
+ owner: &[u8; 32],
+ identifier_type: u8,
+ amount: &[u8; 8],
+ is_minter: u8,
+) -> Option> {
+ if !has_state_block(program) {
+ return None;
+ }
+ let mut p = program.to_vec();
+ p[2..34].copy_from_slice(owner);
+ p[35] = identifier_type;
+ p[37..45].copy_from_slice(amount);
+ p[46] = is_minter;
+ Some(p)
+}
+
+/// blake2b-256 — the hash Kaspa P2SH commitments use (same parameters as
+/// [`crate::p2sh_reveal`]'s verification).
+pub fn blake2b_256(bytes: &[u8]) -> [u8; 32] {
+ let mut out = [0u8; 32];
+ out.copy_from_slice(blake2b_simd::Params::new().hash_length(32).hash(bytes).as_bytes());
+ out
+}
+
+/// Prove a P2SH-committed output's state: splice the candidate fields into a
+/// same-build program and accept iff the spliced program hashes to the
+/// output's committed hash. Returns the proven state, or `None` (fails
+/// closed on wrong build, wrong candidate, or a non-P2SH spk).
+pub fn prove_output_state(
+ base_program: &[u8],
+ output_spk: &[u8],
+ owner: &[u8; 32],
+ identifier_type: u8,
+ amount: &[u8; 8],
+ is_minter: u8,
+) -> Option {
+ let want = p2sh_hash(output_spk)?;
+ let candidate = splice_token_state(base_program, owner, identifier_type, amount, is_minter)?;
+ (blake2b_256(&candidate) == want).then(|| TokenState {
+ owner: *owner,
+ identifier_type,
+ amount_raw: amount.to_vec(),
+ minter_raw: vec![is_minter],
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// All three registered builds: real on-chain reveal programs.
+ fn builds() -> [&'static [u8]; 3] {
+ [
+ include_bytes!("../fixtures/kcc20_a_a.bin").as_slice(),
+ include_bytes!("../fixtures/kcc20_b_a.bin").as_slice(),
+ include_bytes!("../fixtures/kcc20_c_a.bin").as_slice(),
+ ]
+ }
+
+ #[test]
+ fn splice_then_decode_roundtrips_on_all_builds() {
+ let registry = Registry::default();
+ for base in builds() {
+ assert!(has_state_block(base));
+ let owner = [0xabu8; 32];
+ let amount = 71_753i64.to_le_bytes();
+ for (id_type, minter) in [(0x00u8, 0x00u8), (0x02, 0x01)] {
+ let spliced = splice_token_state(base, &owner, id_type, &amount, minter).unwrap();
+ let st = decode_token_state(®istry, 1, &spliced)
+ .expect("spliced program must still decode as KCC20 token");
+ assert_eq!(st.owner, owner);
+ assert_eq!(st.identifier_type, id_type);
+ assert_eq!(st.amount_i64(), Some(71_753));
+ assert_eq!(st.is_minter(), Some(minter == 1));
+ }
+ }
+ }
+
+ #[test]
+ fn prove_output_state_accepts_only_the_committed_state() {
+ let base = builds()[0];
+ let owner = [0x11u8; 32];
+ let amount = 4_000i64.to_le_bytes();
+ let committed = splice_token_state(base, &owner, 0x00, &amount, 0x00).unwrap();
+ let mut spk = vec![0xaa, 0x20];
+ spk.extend_from_slice(&blake2b_256(&committed));
+ spk.push(0x87);
+
+ let st = prove_output_state(base, &spk, &owner, 0x00, &amount, 0x00).unwrap();
+ assert_eq!(st.amount_i64(), Some(4_000));
+ // A single wrong field byte fails closed.
+ assert!(prove_output_state(base, &spk, &owner, 0x02, &amount, 0x00).is_none());
+ let wrong_amount = 4_001i64.to_le_bytes();
+ assert!(prove_output_state(base, &spk, &owner, 0x00, &wrong_amount, 0x00).is_none());
+ // A different build as splice base fails closed too.
+ assert!(prove_output_state(builds()[1], &spk, &owner, 0x00, &amount, 0x00).is_none());
+ }
+
+ #[test]
+ fn amount_strictness() {
+ let mk = |raw: &[u8]| TokenState {
+ owner: [0; 32],
+ identifier_type: 0,
+ amount_raw: raw.to_vec(),
+ minter_raw: vec![0],
+ };
+ assert_eq!(mk(&i64::MAX.to_le_bytes()).amount_i64(), Some(i64::MAX));
+ assert_eq!(mk(&0i64.to_le_bytes()).amount_i64(), Some(0));
+ // Sign bit set = negative script number: out of model, never a u64.
+ assert_eq!(mk(&[0, 0, 0, 0, 0, 0, 0, 0x80]).amount_i64(), None);
+ // Non-8-byte widths are out of model (chain uses fixed 8-byte LE).
+ assert_eq!(mk(&[1, 0, 0, 0]).amount_i64(), None);
+ assert_eq!(mk(&[]).amount_i64(), None);
+ // isMinter strictness
+ let mut st = mk(&1i64.to_le_bytes());
+ st.minter_raw = vec![2];
+ assert_eq!(st.is_minter(), None);
+ st.minter_raw = vec![];
+ assert_eq!(st.is_minter(), None);
+ }
+}
diff --git a/vendor/kascov-preflight/crates/kascov-decode/src/lib.rs b/vendor/kascov-preflight/crates/kascov-decode/src/lib.rs
new file mode 100644
index 0000000..3e985ee
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-decode/src/lib.rs
@@ -0,0 +1,1171 @@
+//! Covenant state decoding. Template-specific decoders are additive; the
+//! always-correct fallback is an opcode disassembly of the state script.
+
+pub mod disasm;
+pub mod kcc1;
+pub mod kcc20;
+pub mod observed;
+
+use disasm::{disassemble, Instruction, OpGroup};
+
+/// A labeled state field extracted by a template decoder.
+#[derive(Clone, Debug, serde::Serialize)]
+pub struct Field {
+ pub name: &'static str,
+ #[serde(serialize_with = "hex_ser")]
+ pub value: Vec,
+}
+
+fn hex_ser(bytes: &[u8], s: S) -> Result {
+ s.serialize_str(&hex::encode(bytes))
+}
+
+/// What a decoder could make of a covenant state script.
+#[derive(Clone, Debug, serde::Serialize)]
+pub struct Decoded {
+ /// Name of the decoder that matched ("disasm" for the fallback).
+ pub decoder: &'static str,
+ pub instructions: Vec,
+ pub truncated: bool,
+ /// Data pushes, in order — for known templates these are the state fields.
+ pub pushes: Vec>,
+ pub uses_covenant_ops: bool,
+ pub uses_zk_ops: bool,
+ /// Best-effort proving system guessed from the ZK arguments, when the
+ /// script uses `OpZkPrecompile` (see `zk_system`). `None` when there are
+ /// no ZK ops or the shape is too ambiguous to call.
+ pub zk_system: Option<&'static str>,
+ /// Recognized template name, when a template decoder matched.
+ pub template: Option<&'static str>,
+ /// Labeled constructor/state fields, when the template names them.
+ pub fields: Vec,
+}
+
+pub trait StateDecoder: Send + Sync {
+ fn name(&self) -> &'static str;
+ /// Return a decode if this decoder recognizes the script template.
+ fn decode(&self, spk_version: u16, script: &[u8]) -> Option;
+}
+
+/// Fallback: full disassembly. Always succeeds.
+pub struct DisasmDecoder;
+
+fn base_decode(name: &'static str, script: &[u8]) -> Decoded {
+ let (instructions, truncated) = disassemble(script);
+ Decoded {
+ decoder: name,
+ pushes: instructions.iter().filter_map(|i| i.data.clone()).collect(),
+ uses_covenant_ops: instructions.iter().any(|i| i.group == OpGroup::Covenant),
+ uses_zk_ops: instructions.iter().any(|i| i.group == OpGroup::Zk),
+ zk_system: zk_system_from(&instructions),
+ instructions,
+ truncated,
+ template: None,
+ fields: vec![],
+ }
+}
+
+/// Guess which zero-knowledge proving system a covenant script hands to
+/// `OpZkPrecompile` (KIP-16, opcode `0xa6`). Best effort: the verifier pops a
+/// verifying key, a proof, and public inputs off the stack, so the data
+/// pushes in the program encode those shapes. We key on the two systems'
+/// very different proof sizes and prefer `None` over a shaky guess.
+///
+/// * **Groth16** (BN254 / BLS12-381) has a fixed, tiny proof — three curve
+/// points, 128–256 bytes depending on curve and compression — and its
+/// public inputs are 32-byte field elements. The verifying key is a
+/// handful of 64-byte G1 / 96–128-byte G2 points. No single push reaches
+/// STARK scale.
+/// * **RISC Zero** seals are STARK receipts: kilobytes of proof data plus a
+/// 32-byte image id. A push of >= 1 KiB feeding the precompile is the tell.
+/// * A push between those bands (300–1023 bytes) is bigger than any Groth16
+/// encoding but below STARK scale — some succinct system we can't
+/// attribute further, labeled "succinct proof (inferred)".
+///
+/// Bare 32/64-byte pushes on their own are too generic to attribute, so those
+/// yield `None` (as does the 257–299 byte gap just above the Groth16 band).
+pub fn zk_system(script: &[u8]) -> Option<&'static str> {
+ let (instructions, _) = disassemble(script);
+ zk_system_from(&instructions)
+}
+
+fn zk_system_from(instructions: &[Instruction]) -> Option<&'static str> {
+ // Only meaningful for scripts that actually invoke the ZK verifier.
+ if !instructions.iter().any(|i| i.group == OpGroup::Zk) {
+ return None;
+ }
+ let sizes: Vec = instructions.iter().filter_map(|i| i.data.as_ref().map(|d| d.len())).collect();
+ // STARK-scale seal → RISC Zero.
+ if sizes.iter().any(|&n| n >= 1024) {
+ return Some("risc0");
+ }
+ // Groth16 proof band: three curve points, ~128 (compressed) up to 256
+ // (uncompressed). A push in this range, with nothing STARK-scale present,
+ // reads as Groth16.
+ if sizes.iter().any(|&n| (128..=256).contains(&n)) {
+ return Some("groth16");
+ }
+ // Above every Groth16 encoding but below STARK scale: some succinct
+ // system's proof, unattributable beyond that.
+ if sizes.iter().any(|&n| (300..1024).contains(&n)) {
+ return Some("succinct proof (inferred)");
+ }
+ None
+}
+
+impl StateDecoder for DisasmDecoder {
+ fn name(&self) -> &'static str {
+ "disasm"
+ }
+ fn decode(&self, _spk_version: u16, script: &[u8]) -> Option {
+ Some(base_decode(self.name(), script))
+ }
+}
+
+/// ` OpCheckSig` — the plain pay-to-pubkey state carried by
+/// most covenants observed on TN10 (and the [[Covenant Lab]] ones).
+pub struct P2pkStateDecoder;
+
+impl StateDecoder for P2pkStateDecoder {
+ fn name(&self) -> &'static str {
+ "p2pk-state"
+ }
+ fn decode(&self, _spk_version: u16, script: &[u8]) -> Option {
+ let ok = matches!(script.len(), 34 | 35)
+ && script[0] as usize == script.len() - 2
+ && script[script.len() - 1] == 0xac;
+ if !ok {
+ return None;
+ }
+ let mut d = base_decode(self.name(), script);
+ d.template = Some("p2pk state");
+ d.fields = vec![Field { name: "owner_pubkey", value: script[1..script.len() - 1].to_vec() }];
+ Some(d)
+ }
+}
+
+/// `OpBlake2b <32-byte hash> OpEqual` — a P2SH commitment: the program is
+/// revealed at spend time (see `p2sh_reveal`).
+pub struct P2shCommitmentDecoder;
+
+impl StateDecoder for P2shCommitmentDecoder {
+ fn name(&self) -> &'static str {
+ "p2sh-commitment"
+ }
+ fn decode(&self, _spk_version: u16, script: &[u8]) -> Option {
+ let hash = p2sh_hash(script)?.to_vec();
+ let mut d = base_decode(self.name(), script);
+ d.template = Some("p2sh commitment");
+ d.fields = vec![Field { name: "program_hash", value: hash }];
+ Some(d)
+ }
+}
+
+/// One position in a compiled-contract skeleton. SilverScript inlines
+/// constructor arguments at their use sites (an argument can appear several
+/// times mid-script), so templates are matched on the disassembled
+/// instruction stream: fixed opcodes and constant pushes must be identical,
+/// argument slots accept any push and yield labeled fields.
+enum SkelItem {
+ /// A non-push instruction that must match exactly.
+ Op(u8),
+ /// A push whose bytes are part of the template itself. `raw` keeps the
+ /// original encoding so re-emitting a contract is byte-identical even
+ /// where the compiler chose a non-minimal push.
+ ConstPush { value: Vec, raw: Vec },
+ /// A push carrying a constructor argument.
+ Slot(&'static str),
+}
+
+/// Canonical push encoding — the bytes the SilverScript compiler's
+/// ScriptBuilder emits for a pushed value. Mirrors `encodePush` in
+/// `web/disasm.js`; used to re-encode argument slots when emitting a contract.
+pub fn encode_push(value: &[u8]) -> Vec {
+ match value {
+ [] => vec![0x00], // OpFalse
+ [0x81] => vec![0x4f], // Op1Negate
+ [v] if (1..=16).contains(v) => vec![0x50 + v], // Op1..Op16
+ _ if value.len() <= 75 => {
+ let mut out = Vec::with_capacity(value.len() + 1);
+ out.push(value.len() as u8);
+ out.extend_from_slice(value);
+ out
+ }
+ _ if value.len() <= 0xff => {
+ let mut out = vec![0x4c, value.len() as u8];
+ out.extend_from_slice(value);
+ out
+ }
+ _ if value.len() <= 0xffff => {
+ let mut out = vec![0x4d, (value.len() & 0xff) as u8, (value.len() >> 8) as u8];
+ out.extend_from_slice(value);
+ out
+ }
+ _ => {
+ let n = value.len() as u32;
+ let mut out = vec![0x4e, (n & 0xff) as u8, (n >> 8 & 0xff) as u8, (n >> 16 & 0xff) as u8, (n >> 24) as u8];
+ out.extend_from_slice(value);
+ out
+ }
+ }
+}
+
+pub struct Skeleton {
+ pub name: &'static str,
+ items: Vec,
+ /// Field labels in constructor order (for display ordering).
+ param_order: Vec<&'static str>,
+}
+
+/// A push instruction's value, whether it's a data push or a small-int
+/// opcode (`OpFalse`/`Op1Negate`/`Op1..Op16`), in script-number encoding.
+fn push_value(inst: &Instruction) -> Option> {
+ if let Some(data) = &inst.data {
+ return Some(data.clone());
+ }
+ match inst.opcode {
+ 0x00 => Some(vec![]),
+ 0x4f => Some(vec![0x81]),
+ 0x51..=0x60 => Some(vec![inst.opcode - 0x50]),
+ _ => None,
+ }
+}
+
+fn is_push(inst: &Instruction) -> bool {
+ inst.group == OpGroup::Push
+}
+
+impl Skeleton {
+ /// Derive a skeleton from two builds of the same contract with different
+ /// sentinel arguments. Instructions must align one-to-one: equal
+ /// non-push opcodes stay fixed, equal pushes become constants, and
+ /// differing pushes become slots — labeled by looking the first build's
+ /// value up in `sentinels` (constructor order).
+ pub fn derive(
+ name: &'static str,
+ a: &[u8],
+ b: &[u8],
+ sentinels: &[(&'static str, Vec)],
+ ) -> Option {
+ let (ia, ta) = disassemble(a);
+ let (ib, tb) = disassemble(b);
+ if ta || tb || ia.len() != ib.len() {
+ return None;
+ }
+ let mut items = Vec::with_capacity(ia.len());
+ for (i, (x, y)) in ia.iter().zip(&ib).enumerate() {
+ match (is_push(x), is_push(y)) {
+ (false, false) => {
+ if x.opcode != y.opcode {
+ return None;
+ }
+ items.push(SkelItem::Op(x.opcode));
+ }
+ (true, true) => {
+ let vx = push_value(x)?;
+ let vy = push_value(y)?;
+ if vx == vy {
+ // raw span of this push in dump A, for byte-perfect emit
+ let end = ia.get(i + 1).map_or(a.len(), |n| n.offset);
+ items.push(SkelItem::ConstPush { value: vx, raw: a[x.offset..end].to_vec() });
+ } else {
+ let (label, _) = sentinels.iter().find(|(_, s)| *s == vx)?;
+ items.push(SkelItem::Slot(label));
+ }
+ }
+ _ => return None,
+ }
+ }
+ Some(Skeleton {
+ name,
+ items,
+ param_order: sentinels.iter().map(|(l, _)| *l).collect(),
+ })
+ }
+
+ /// Derive a skeleton from two or more distinct on-chain instances of the
+ /// same compiled contract (no sentinels available — the arguments are
+ /// whatever the deployers used). Instructions must align one-to-one
+ /// across every instance: equal non-push opcodes stay fixed, pushes that
+ /// agree everywhere become constants, and pushes that differ anywhere
+ /// become slots. Slots are labeled in first-occurrence order; two slot
+ /// positions whose values agree in *every* instance are the same inlined
+ /// argument and share one label (and `match_script` will keep enforcing
+ /// that they agree). `labels` must name exactly the distinct slots.
+ pub fn derive_observed(
+ name: &'static str,
+ instances: &[&[u8]],
+ labels: &[&'static str],
+ ) -> Option {
+ if instances.len() < 2 {
+ return None;
+ }
+ let mut streams = Vec::with_capacity(instances.len());
+ for bytes in instances {
+ let (insts, truncated) = disassemble(bytes);
+ if truncated || streams.first().is_some_and(|f: &Vec| f.len() != insts.len()) {
+ return None;
+ }
+ streams.push(insts);
+ }
+ let first = &streams[0];
+ let mut items = Vec::with_capacity(first.len());
+ // Distinct slots seen so far, as their value-vector across instances.
+ let mut slots: Vec>> = Vec::new();
+ for (i, inst) in first.iter().enumerate() {
+ if !is_push(inst) {
+ if streams.iter().any(|s| is_push(&s[i]) || s[i].opcode != inst.opcode) {
+ return None;
+ }
+ items.push(SkelItem::Op(inst.opcode));
+ continue;
+ }
+ let vector = streams
+ .iter()
+ .map(|s| push_value(&s[i]))
+ .collect::>>()?;
+ if vector.iter().all(|v| *v == vector[0]) {
+ let end = first.get(i + 1).map_or(instances[0].len(), |n| n.offset);
+ items.push(SkelItem::ConstPush {
+ value: vector[0].clone(),
+ raw: instances[0][inst.offset..end].to_vec(),
+ });
+ } else {
+ let slot = match slots.iter().position(|s| *s == vector) {
+ Some(idx) => idx,
+ None => {
+ slots.push(vector);
+ slots.len() - 1
+ }
+ };
+ items.push(SkelItem::Slot(labels.get(slot).copied()?));
+ }
+ }
+ // Every label must correspond to an actual slot — a mismatch means
+ // the fixtures (or the labels) are wrong.
+ if slots.len() != labels.len() {
+ return None;
+ }
+ Some(Skeleton { name, items, param_order: labels.to_vec() })
+ }
+
+ /// Constructor parameter labels, in order.
+ pub fn params(&self) -> &[&'static str] {
+ &self.param_order
+ }
+
+ /// Re-emit this contract's compiled bytes with new constructor arguments.
+ /// Fixed ops and constant pushes stay byte-identical to the original
+ /// build; each slot is re-encoded from `args` (looked up by label).
+ /// Returns None if an argument is missing. The inverse of `match_script`:
+ /// `match_script(disassemble(emit(args))) == args`.
+ pub fn emit(&self, args: &[(&str, &[u8])]) -> Option> {
+ let mut out = Vec::new();
+ for item in &self.items {
+ match item {
+ SkelItem::Op(op) => out.push(*op),
+ SkelItem::ConstPush { raw, .. } => out.extend_from_slice(raw),
+ SkelItem::Slot(label) => {
+ let (_, value) = args.iter().find(|(l, _)| l == label)?;
+ out.extend_from_slice(&encode_push(value));
+ }
+ }
+ }
+ Some(out)
+ }
+
+ /// Match a script against this skeleton; on success return its fields in
+ /// constructor order. Repeated slots of the same argument must agree.
+ fn match_script(&self, instructions: &[Instruction]) -> Option> {
+ if instructions.len() != self.items.len() {
+ return None;
+ }
+ let mut values: Vec<(&'static str, Vec)> = Vec::new();
+ for (item, inst) in self.items.iter().zip(instructions) {
+ if !match_skel_item(item, inst, &mut values) {
+ return None;
+ }
+ }
+ Some(fields_in_order(&self.param_order, &values))
+ }
+}
+
+/// Match one skeleton item against one instruction. Slot values accumulate in
+/// `values`; a label seen twice within the same scope must carry the same
+/// value (SilverScript inlines an argument at every use site).
+fn match_skel_item(
+ item: &SkelItem,
+ inst: &Instruction,
+ values: &mut Vec<(&'static str, Vec)>,
+) -> bool {
+ match item {
+ SkelItem::Op(op) => !is_push(inst) && inst.opcode == *op,
+ SkelItem::ConstPush { value, .. } => push_value(inst).as_ref() == Some(value),
+ SkelItem::Slot(label) => {
+ let Some(v) = push_value(inst) else { return false };
+ match values.iter().find(|(l, _)| l == label) {
+ Some((_, prev)) => *prev == v,
+ None => {
+ values.push((label, v));
+ true
+ }
+ }
+ }
+ }
+}
+
+fn fields_in_order(order: &[&'static str], values: &[(&'static str, Vec)]) -> Vec {
+ order
+ .iter()
+ .filter_map(|label| {
+ values.iter().find(|(l, _)| l == label).map(|(_, v)| Field { name: label, value: v.clone() })
+ })
+ .collect()
+}
+
+/// Push-size-aware shape equality: two instructions align when both are
+/// pushes of the same width or both are the same non-push opcode. This is
+/// the alignment used to find the repeated block of a variable-arity family.
+fn same_shape(a: &Instruction, b: &Instruction) -> bool {
+ match (push_value(a), push_value(b)) {
+ (Some(x), Some(y)) => x.len() == y.len(),
+ (None, None) => a.opcode == b.opcode,
+ _ => false,
+ }
+}
+
+/// A compiled-contract family whose builds differ only by how many times one
+/// instruction block repeats (e.g. genesis0's slot-mint emits one
+/// amount+script check per collection output). Matched as
+/// `prefix · group×N · suffix` with `N >= min_repeats`; the group's pushes
+/// are per-repeat slots, so every arity of the family decodes to one name.
+pub struct RepeatSkeleton {
+ pub name: &'static str,
+ prefix: Vec,
+ group: Vec,
+ suffix: Vec,
+ min_repeats: usize,
+ param_order: Vec<&'static str>,
+ group_params: Vec<&'static str>,
+}
+
+impl RepeatSkeleton {
+ /// Derive from real instances of two different arities: `long` holds two
+ /// or more instances of the bigger build, `short` at least one of the
+ /// smaller. The repeated group is the shape difference between the two
+ /// arities (rotated to its leftmost position, so trailing copies inside
+ /// the longer build's aligned prefix fold into repeats). Fixed-part
+ /// pushes become constants only when *every* instance of *both* arities
+ /// agrees — arity-dependent constants (output counts, indexes) become
+ /// slots automatically. `labels` names the distinct fixed-part slots in
+ /// first-occurrence order; `group_labels` names the group's pushes in
+ /// order (repeat a label to require equality within one repeat).
+ pub fn derive(
+ name: &'static str,
+ long: &[&[u8]],
+ short: &[&[u8]],
+ labels: &[&'static str],
+ group_labels: &[&'static str],
+ ) -> Option {
+ if long.len() < 2 || short.is_empty() {
+ return None;
+ }
+ let parse = |set: &[&[u8]]| -> Option>> {
+ let mut streams = Vec::with_capacity(set.len());
+ for bytes in set {
+ let (insts, truncated) = disassemble(bytes);
+ if truncated || streams.first().is_some_and(|f: &Vec| f.len() != insts.len()) {
+ return None;
+ }
+ streams.push(insts);
+ }
+ Some(streams)
+ };
+ let la = parse(long)?;
+ let lb = parse(short)?;
+ let (a0, b0) = (&la[0], &lb[0]);
+ let g = a0.len().checked_sub(b0.len()).filter(|g| *g > 0)?;
+ // Shape-align the two arities from both ends, then rotate the group
+ // window as far left as it goes so it sits at the first repeat.
+ let mut p = 0;
+ while p < b0.len() && same_shape(&a0[p], &b0[p]) {
+ p += 1;
+ }
+ let mut s = 0;
+ while s < b0.len() - p && same_shape(&a0[a0.len() - 1 - s], &b0[b0.len() - 1 - s]) {
+ s += 1;
+ }
+ if a0.len() - p - s < g {
+ return None;
+ }
+ p = a0.len() - s - g; // group directly before the suffix…
+ while p > 0 && same_shape(&a0[p - 1], &a0[p + g - 1]) {
+ p -= 1; // …rotated to its leftmost equivalent position
+ }
+ let extra = b0.len().checked_sub(p + s)?;
+ if extra % g != 0 {
+ return None;
+ }
+ let min_repeats = extra / g;
+
+ // Fixed parts: const/slot decided across every instance of both
+ // arities (suffix positions aligned from the end).
+ let mut slots: Vec>> = Vec::new();
+ let mut build = |positions: &mut dyn Iterator- | -> Option
> {
+ let mut items = Vec::new();
+ for (ia, ib) in positions {
+ let inst = &a0[ia];
+ if !is_push(inst) {
+ let ok = la.iter().all(|x| !is_push(&x[ia]) && x[ia].opcode == inst.opcode)
+ && lb.iter().all(|x| !is_push(&x[ib]) && x[ib].opcode == inst.opcode);
+ if !ok {
+ return None;
+ }
+ items.push(SkelItem::Op(inst.opcode));
+ continue;
+ }
+ let vector = la
+ .iter()
+ .map(|x| push_value(&x[ia]))
+ .chain(lb.iter().map(|x| push_value(&x[ib])))
+ .collect::>>()?;
+ if vector.iter().all(|v| *v == vector[0]) {
+ let end = a0.get(ia + 1).map_or(long[0].len(), |n| n.offset);
+ items.push(SkelItem::ConstPush {
+ value: vector[0].clone(),
+ raw: long[0][inst.offset..end].to_vec(),
+ });
+ } else {
+ let slot = match slots.iter().position(|x| *x == vector) {
+ Some(idx) => idx,
+ None => {
+ slots.push(vector);
+ slots.len() - 1
+ }
+ };
+ items.push(SkelItem::Slot(labels.get(slot).copied()?));
+ }
+ }
+ Some(items)
+ };
+ let prefix = build(&mut (0..p).map(|i| (i, i)))?;
+ let suffix = build(&mut (0..s).map(|j| (a0.len() - s + j, b0.len() - s + j)))?;
+ if slots.len() != labels.len() {
+ return None;
+ }
+
+ // The group: every push is a per-repeat slot.
+ let mut group = Vec::with_capacity(g);
+ let mut pushes = 0;
+ for inst in &a0[p..p + g] {
+ if is_push(inst) {
+ group.push(SkelItem::Slot(*group_labels.get(pushes)?));
+ pushes += 1;
+ } else {
+ group.push(SkelItem::Op(inst.opcode));
+ }
+ }
+ if pushes != group_labels.len() {
+ return None;
+ }
+ let mut group_params: Vec<&'static str> = Vec::new();
+ for label in group_labels {
+ if !group_params.contains(label) {
+ group_params.push(label);
+ }
+ }
+
+ let skel = RepeatSkeleton {
+ name,
+ prefix,
+ group,
+ suffix,
+ min_repeats,
+ param_order: labels.to_vec(),
+ group_params,
+ };
+ // Nothing is registered on faith: the derived matcher must accept
+ // every instance it was derived from.
+ for bytes in long.iter().chain(short) {
+ let (insts, _) = disassemble(bytes);
+ skel.match_script(&insts)?;
+ }
+ Some(skel)
+ }
+
+ /// Fixed-part parameter labels, in order.
+ pub fn params(&self) -> &[&'static str] {
+ &self.param_order
+ }
+
+ /// Per-repeat parameter labels, in order.
+ pub fn group_params(&self) -> &[&'static str] {
+ &self.group_params
+ }
+
+ /// Match `prefix · group×N · suffix`; fixed-part fields come first (in
+ /// `params()` order), then each repeat's fields in repeat order.
+ fn match_script(&self, instructions: &[Instruction]) -> Option> {
+ let fixed = self.prefix.len() + self.suffix.len();
+ let extra = instructions.len().checked_sub(fixed)?;
+ if self.group.is_empty() || extra % self.group.len() != 0 {
+ return None;
+ }
+ let repeats = extra / self.group.len();
+ if repeats < self.min_repeats {
+ return None;
+ }
+ let mut values: Vec<(&'static str, Vec)> = Vec::new();
+ for (item, inst) in self.prefix.iter().zip(instructions) {
+ if !match_skel_item(item, inst, &mut values) {
+ return None;
+ }
+ }
+ let mut at = self.prefix.len();
+ let mut repeat_fields: Vec = Vec::new();
+ for _ in 0..repeats {
+ // Fresh scope per repeat: a repeated label must agree within one
+ // repeat (an output index used twice) but may differ across them.
+ let mut rv: Vec<(&'static str, Vec)> = Vec::new();
+ for item in &self.group {
+ if !match_skel_item(item, &instructions[at], &mut rv) {
+ return None;
+ }
+ at += 1;
+ }
+ repeat_fields.extend(fields_in_order(&self.group_params, &rv));
+ }
+ for (item, inst) in self.suffix.iter().zip(&instructions[at..]) {
+ if !match_skel_item(item, inst, &mut values) {
+ return None;
+ }
+ }
+ let mut fields = fields_in_order(&self.param_order, &values);
+ fields.append(&mut repeat_fields);
+ Some(fields)
+ }
+}
+
+/// Matches compiled contracts against known skeletons.
+pub struct TemplateDecoder {
+ skeletons: Vec,
+ repeats: Vec,
+}
+
+impl TemplateDecoder {
+ pub fn new(skeletons: Vec) -> Self {
+ Self { skeletons, repeats: vec![] }
+ }
+
+ pub fn with_repeats(skeletons: Vec, repeats: Vec) -> Self {
+ Self { skeletons, repeats }
+ }
+}
+
+impl StateDecoder for TemplateDecoder {
+ fn name(&self) -> &'static str {
+ "template"
+ }
+ fn decode(&self, _spk_version: u16, script: &[u8]) -> Option {
+ let (instructions, truncated) = disassemble(script);
+ if truncated {
+ return None;
+ }
+ let hit = self
+ .skeletons
+ .iter()
+ .find_map(|s| s.match_script(&instructions).map(|f| (s.name, f)))
+ .or_else(|| {
+ self.repeats.iter().find_map(|s| s.match_script(&instructions).map(|f| (s.name, f)))
+ });
+ let (name, fields) = hit?;
+ let mut d = base_decode("template", script);
+ d.template = Some(name);
+ d.fields = fields;
+ Some(d)
+ }
+}
+
+/* ------------------------------------------------ SilverScript templates
+ The example contracts from kaspanet/silverscript
+ (silverscript-lang/tests/examples), each compiled twice with sentinel
+ constructor arguments via `compile_contract` — skeletons derive at
+ registration and stay aligned with these exact dumps. */
+
+const SENT_A32: [u8; 32] = [0x11; 32];
+const SENT_B32: [u8; 32] = [0x22; 32];
+const SENT_C32: [u8; 32] = [0x33; 32];
+const SENT_D32: [u8; 32] = [0x44; 32];
+
+const MECENAS_A: &str = "6b6c76009c637502e803b100c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e876902e803b9be760400e1f50594527994760400e1f505547993a16300c252795479949c696700c20400e1f5059c6951c3b9bf876951c2789c6968007a75007a75007a75516776519c637578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac69757551677500696868";
+const MECENAS_B: &str = "6b6c76009c637502d007b100c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e876902e803b9be760480b2e60e94527994760480b2e60e547993a16300c252795479949c696700c20480b2e60e9c6951c3b9bf876951c2789c6968007a75007a75007a75516776519c637578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac69757551677500696868";
+const ESCROW_A: &str = "78aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac6900c2b9be02e803949c6900c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e8700c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e879b69757551";
+const ESCROW_B: &str = "78aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac6900c2b9be02e803949c6900c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e8700c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e879b69757551";
+const LASTWILL_A: &str = "6b6c76009c637502b400b178aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac697575516776519c637578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac697575516776529c637578aa2011111111111111111111111111111111111111111111111111111111111111118769765279ac6900c2b9be02e803949c6900c3b9bf876975755167750069686868";
+const LASTWILL_B: &str = "6b6c76009c637502b400b178aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac697575516776519c637578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac697575516776529c637578aa2022222222222222222222222222222222222222222222222222222222222222228769765279ac6900c2b9be02e803949c6900c3b9bf876975755167750069686868";
+
+/// Minimal script-number encoding of the sentinel ints used in the dumps.
+/// Minimal script-number (little-endian, sign-guard) encoding of a
+/// non-negative integer — used for pledge/period args and entrypoint
+/// selectors when emitting a contract or building a spend witness.
+pub fn snum(v: i64) -> Vec {
+ let mut out = Vec::new();
+ let mut abs = v.unsigned_abs();
+ while abs > 0 {
+ out.push((abs & 0xff) as u8);
+ abs >>= 8;
+ }
+ if let Some(last) = out.last() {
+ if last & 0x80 != 0 {
+ out.push(0);
+ }
+ }
+ out
+}
+
+pub fn silverscript_skeletons() -> Vec {
+ let hex2 = |s: &str| hex::decode(s).expect("template dump hex");
+ let mut out = Vec::new();
+ // contract Mecenas(pubkey recipient, byte[32] funder, int pledge, int period)
+ if let Some(s) = Skeleton::derive(
+ "SilverScript · Mecenas",
+ &hex2(MECENAS_A),
+ &hex2(MECENAS_B),
+ &[
+ ("recipient", SENT_A32.to_vec()),
+ ("funder_hash", SENT_C32.to_vec()),
+ ("pledge", snum(100_000_000)),
+ ("period", snum(1000)),
+ ],
+ ) {
+ out.push(s);
+ }
+ // contract Escrow(byte[32] arbiter, pubkey buyer, pubkey seller)
+ if let Some(s) = Skeleton::derive(
+ "SilverScript · Escrow",
+ &hex2(ESCROW_A),
+ &hex2(ESCROW_B),
+ &[
+ ("arbiter_hash", SENT_C32.to_vec()),
+ ("buyer", SENT_A32.to_vec()),
+ ("seller", SENT_B32.to_vec()),
+ ],
+ ) {
+ out.push(s);
+ }
+ // contract LastWill(byte[32] inheritor, byte[32] cold, byte[32] hot)
+ if let Some(s) = Skeleton::derive(
+ "SilverScript · LastWill",
+ &hex2(LASTWILL_A),
+ &hex2(LASTWILL_B),
+ &[
+ ("inheritor_hash", SENT_C32.to_vec()),
+ ("cold_hash", SENT_D32.to_vec()),
+ ("hot_hash", SENT_A32.to_vec()),
+ ],
+ ) {
+ out.push(s);
+ }
+ out
+}
+
+/// The committed hash of a canonical Kaspa P2SH script-public-key
+/// (`OpBlake2b OpData32 OpEqual`), if `spk` has that shape.
+pub fn p2sh_hash(spk: &[u8]) -> Option<&[u8]> {
+ (spk.len() == 35 && spk[0] == 0xaa && spk[1] == 0x20 && spk[34] == 0x87)
+ .then(|| &spk[2..34])
+}
+
+/// Spend-time reveal: when a P2SH state UTXO is spent, the signature
+/// script's final push is the program the covenant actually ran. Returns it
+/// only if its blake2b-256 matches the committed hash.
+pub fn p2sh_reveal(spk: &[u8], sig_script: &[u8]) -> Option> {
+ let hash = p2sh_hash(spk)?;
+ let (instructions, truncated) = disassemble(sig_script);
+ if truncated {
+ return None;
+ }
+ let redeem = instructions.last()?.data.clone()?;
+ let digest = blake2b_simd::Params::new().hash_length(32).hash(&redeem);
+ (digest.as_bytes() == hash).then_some(redeem)
+}
+
+/// Try registered decoders in order, ending with the disassembly fallback.
+pub struct Registry {
+ decoders: Vec>,
+}
+
+impl Default for Registry {
+ fn default() -> Self {
+ let mut skeletons = silverscript_skeletons();
+ skeletons.extend(observed::observed_skeletons());
+ Self {
+ decoders: vec![
+ Box::new(TemplateDecoder::with_repeats(skeletons, observed::observed_repeat_skeletons())),
+ Box::new(P2pkStateDecoder),
+ Box::new(P2shCommitmentDecoder),
+ ],
+ }
+ }
+}
+
+impl Registry {
+ pub fn register(&mut self, decoder: Box) {
+ self.decoders.push(decoder);
+ }
+
+ pub fn decode(&self, spk_version: u16, script: &[u8]) -> Decoded {
+ self.decoders
+ .iter()
+ .find_map(|d| d.decode(spk_version, script))
+ .or_else(|| DisasmDecoder.decode(spk_version, script))
+ .expect("disasm fallback always decodes")
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn p2pk_state_labels_owner() {
+ let mut script = vec![0x20];
+ script.extend([0x7f; 32]);
+ script.push(0xac);
+ let d = Registry::default().decode(0, &script);
+ assert_eq!(d.template, Some("p2pk state"));
+ assert_eq!(d.fields.len(), 1);
+ assert_eq!(d.fields[0].name, "owner_pubkey");
+ assert_eq!(d.fields[0].value, vec![0x7f; 32]);
+ }
+
+ #[test]
+ fn p2sh_commitment_labels_hash() {
+ let mut script = vec![0xaa, 0x20];
+ script.extend([0x42; 32]);
+ script.push(0x87);
+ let d = Registry::default().decode(0, &script);
+ assert_eq!(d.template, Some("p2sh commitment"));
+ assert_eq!(d.fields[0].name, "program_hash");
+ assert_eq!(d.fields[0].value, vec![0x42; 32]);
+ }
+
+ #[test]
+ fn all_silverscript_skeletons_derive() {
+ let names: Vec<_> = silverscript_skeletons().iter().map(|s| s.name).collect();
+ assert_eq!(
+ names,
+ [
+ "SilverScript · Mecenas",
+ "SilverScript · Escrow",
+ "SilverScript · LastWill"
+ ],
+ "every embedded compiler dump must derive a skeleton"
+ );
+ }
+
+ #[test]
+ fn skeleton_matches_real_compiled_instances_and_labels_args() {
+ let reg = Registry::default();
+
+ // Mecenas instance B: sentinel args flipped vs A
+ let d = reg.decode(0, &hex::decode(MECENAS_B).unwrap());
+ assert_eq!(d.template, Some("SilverScript · Mecenas"));
+ let get = |n: &str| d.fields.iter().find(|f| f.name == n).map(|f| f.value.clone());
+ assert_eq!(get("recipient"), Some(vec![0x22; 32]));
+ assert_eq!(get("funder_hash"), Some(vec![0x44; 32]));
+ assert_eq!(get("pledge"), Some(snum(250_000_000)));
+ assert_eq!(get("period"), Some(snum(2000)));
+
+ // Escrow A: arbiter/buyer/seller land on the right labels even though
+ // buyer/seller swap between the two builds
+ let d = reg.decode(0, &hex::decode(ESCROW_A).unwrap());
+ assert_eq!(d.template, Some("SilverScript · Escrow"));
+ let get = |n: &str| d.fields.iter().find(|f| f.name == n).map(|f| f.value.clone());
+ assert_eq!(get("arbiter_hash"), Some(vec![0x33; 32]));
+ assert_eq!(get("buyer"), Some(vec![0x11; 32]));
+ assert_eq!(get("seller"), Some(vec![0x22; 32]));
+
+ // LastWill B
+ let d = reg.decode(0, &hex::decode(LASTWILL_B).unwrap());
+ assert_eq!(d.template, Some("SilverScript · LastWill"));
+
+ // one flipped opcode → no template, falls back to plain disasm
+ let mut broken = hex::decode(MECENAS_A).unwrap();
+ let last = broken.len() - 1;
+ broken[last] = 0x51;
+ let d = reg.decode(0, &broken);
+ assert_eq!(d.template, None);
+ }
+
+ #[test]
+ fn emit_with_sentinel_args_reproduces_each_dump() {
+ // emitting with the sentinel args must reproduce dump A byte-for-byte
+ let cases: &[(&str, &str, &[(&str, Vec)])] = &[
+ (
+ "SilverScript · Mecenas",
+ MECENAS_A,
+ &[
+ ("recipient", vec![0x11; 32]),
+ ("funder_hash", vec![0x33; 32]),
+ ("pledge", snum(100_000_000)),
+ ("period", snum(1000)),
+ ],
+ ),
+ (
+ "SilverScript · Escrow",
+ ESCROW_A,
+ &[("arbiter_hash", vec![0x33; 32]), ("buyer", vec![0x11; 32]), ("seller", vec![0x22; 32])],
+ ),
+ (
+ "SilverScript · LastWill",
+ LASTWILL_A,
+ &[("inheritor_hash", vec![0x33; 32]), ("cold_hash", vec![0x44; 32]), ("hot_hash", vec![0x11; 32])],
+ ),
+ ];
+ let skels = silverscript_skeletons();
+ for (name, dump, args) in cases {
+ let skel = skels.iter().find(|s| s.name == *name).expect("skeleton");
+ let args_ref: Vec<(&str, &[u8])> = args.iter().map(|(l, v)| (*l, v.as_slice())).collect();
+ let emitted = skel.emit(&args_ref).expect("emit");
+ assert_eq!(hex::encode(&emitted), *dump, "{name} emit != dump");
+ }
+ }
+
+ #[test]
+ fn emit_round_trips_fresh_args_including_small_int_selector() {
+ let reg = Registry::default();
+ let skels = silverscript_skeletons();
+ let mecenas = skels.iter().find(|s| s.name == "SilverScript · Mecenas").unwrap();
+ // fresh args, pledge with a sign-guard byte (180 -> b4 00), period a small int (6 -> Op6)
+ let recipient = vec![0xab; 32];
+ let funder = vec![0xcd; 32];
+ let pledge = snum(180);
+ let period = snum(6);
+ let args: Vec<(&str, &[u8])> = vec![
+ ("recipient", &recipient),
+ ("funder_hash", &funder),
+ ("pledge", &pledge),
+ ("period", &period),
+ ];
+ let emitted = mecenas.emit(&args).expect("emit");
+ let d = reg.decode(0, &emitted);
+ assert_eq!(d.template, Some("SilverScript · Mecenas"));
+ let get = |n: &str| d.fields.iter().find(|f| f.name == n).map(|f| f.value.clone());
+ assert_eq!(get("recipient"), Some(recipient));
+ assert_eq!(get("funder_hash"), Some(funder));
+ assert_eq!(get("pledge"), Some(snum(180)));
+ assert_eq!(get("period"), Some(snum(6)));
+ // period=6 must encode to Op6 (0x56), the canonical small-int push
+ assert_eq!(encode_push(&snum(6)), vec![0x56]);
+ // and a 32-byte value uses a direct length-prefixed push
+ assert_eq!(encode_push(&[0xab; 32])[0], 0x20);
+ }
+
+ #[test]
+ fn zk_system_classifier() {
+ // No ZK op → nothing to say.
+ let mut plain = vec![0x20];
+ plain.extend([0x00; 32]);
+ plain.push(0xac);
+ assert_eq!(zk_system(&plain), None);
+ assert_eq!(Registry::default().decode(0, &plain).zk_system, None);
+
+ // A ~192-byte proof push (Groth16 band) then OpZkPrecompile → groth16.
+ let mut groth = encode_push(&vec![0x01; 192]);
+ groth.push(0xa6);
+ assert_eq!(zk_system(&groth), Some("groth16"));
+ assert!(Registry::default().decode(0, &groth).uses_zk_ops);
+ assert_eq!(Registry::default().decode(0, &groth).zk_system, Some("groth16"));
+
+ // A 2 KiB seal push → STARK scale → risc0 (wins over any small push).
+ let mut risc0 = encode_push(&vec![0xab; 2048]);
+ risc0.extend(encode_push(&vec![0xcd; 32])); // image id
+ risc0.push(0xa6);
+ assert_eq!(zk_system(&risc0), Some("risc0"));
+
+ // ZK op present but only generic 32-byte pushes → too ambiguous.
+ let mut ambiguous = encode_push(&vec![0x07; 32]);
+ ambiguous.push(0xa6);
+ assert_eq!(zk_system(&ambiguous), None);
+ }
+
+ #[test]
+ fn observed_skeletons_all_derive() {
+ let names: Vec<_> = observed::observed_skeletons().iter().map(|s| s.name).collect();
+ assert_eq!(
+ names,
+ [
+ "PURE",
+ "genesis0 · list",
+ "genesis0 · buy",
+ "genesis0 · list",
+ "genesis0 · buy",
+ "genesis0 · collection",
+ "KCC20 token",
+ "KCC20 token",
+ "KCC20 token",
+ "KCC20 minter",
+ ],
+ "every on-chain fixture pair must derive a skeleton"
+ );
+ let repeats: Vec<_> = observed::observed_repeat_skeletons().iter().map(|s| s.name).collect();
+ assert_eq!(repeats, ["genesis0 · slot-mint"]);
+ }
+
+ #[test]
+ fn observed_families_match_their_fixture_programs() {
+ let reg = Registry::default();
+ let get = |d: &Decoded, n: &str| {
+ d.fields.iter().find(|f| f.name == n).map(|f| f.value.clone())
+ };
+
+ // PURE: the one inlined argument is the CheckSigFromStack key.
+ let pure = include_bytes!("../fixtures/pure_a.bin");
+ let d = reg.decode(0, pure);
+ assert_eq!(d.template, Some("PURE"));
+ assert_eq!(
+ get(&d, "signer_pubkey").map(hex::encode).as_deref(),
+ Some("4df3c68074217004ad86fca1e63b91b73e625d9140063f21992231fdfdfa8936")
+ );
+
+ // KCC20 token: state fields land on the contract's labels. Fixture
+ // kcc20_b_a is a mint-capable instance owned by a covenant id.
+ let d = reg.decode(0, include_bytes!("../fixtures/kcc20_b_a.bin"));
+ assert_eq!(d.template, Some("KCC20 token"));
+ assert_eq!(get(&d, "owner_identifier").map(|v| v.len()), Some(32));
+ assert_eq!(get(&d, "identifier_type"), Some(vec![0x02]));
+ assert_eq!(get(&d, "amount"), Some(vec![0; 8]));
+ assert_eq!(get(&d, "is_minter"), Some(vec![0x01]));
+ // …and kcc20_a_a is a plain pubkey-owned, non-minting instance.
+ let d = reg.decode(0, include_bytes!("../fixtures/kcc20_a_a.bin"));
+ assert_eq!(d.template, Some("KCC20 token"));
+ assert_eq!(get(&d, "identifier_type"), Some(vec![0x00]));
+ assert_eq!(get(&d, "amount").map(hex::encode).as_deref(), Some("a00f000000000000"));
+ assert_eq!(get(&d, "is_minter"), Some(vec![0x00]));
+
+ // KCC20 minter: the input-side and output-side covenant-id pins fold
+ // into one slot per governed token, so exactly two id fields.
+ let d = reg.decode(0, include_bytes!("../fixtures/kcc20_minter_a.bin"));
+ assert_eq!(d.template, Some("KCC20 minter"));
+ assert_eq!(d.fields.len(), 2);
+ assert!(d.fields.iter().all(|f| f.value.len() == 32));
+
+ // Marketplace stages + collection registry.
+ for (fixture, want) in [
+ (include_bytes!("../fixtures/g0_list_v1_a.bin").as_slice(), "genesis0 · list"),
+ (include_bytes!("../fixtures/g0_buy_v1_a.bin").as_slice(), "genesis0 · buy"),
+ (include_bytes!("../fixtures/g0_list_v2_b.bin").as_slice(), "genesis0 · list"),
+ (include_bytes!("../fixtures/g0_buy_v2_b.bin").as_slice(), "genesis0 · buy"),
+ (include_bytes!("../fixtures/g0_col_a.bin").as_slice(), "genesis0 · collection"),
+ ] {
+ assert_eq!(reg.decode(0, fixture).template, Some(want), "fixture for {want}");
+ }
+ // The list program embeds the follow-up buy state's template bytes.
+ let d = reg.decode(0, include_bytes!("../fixtures/g0_list_v1_a.bin"));
+ let tmpl = get(&d, "next_state_template").expect("next_state_template");
+ assert_eq!(tmpl.len(), 396);
+ }
+
+ #[test]
+ fn slot_mint_repeat_matcher_covers_all_arities() {
+ let reg = Registry::default();
+ let get_all = |d: &Decoded, n: &str| {
+ d.fields.iter().filter(|f| f.name == n).map(|f| f.value.clone()).collect::>()
+ };
+
+ // DI4M2 build: two per-collection output checks.
+ let di4m = include_bytes!("../fixtures/slot_mint_di4m_a.bin");
+ let d = reg.decode(0, di4m);
+ assert_eq!(d.template, Some("genesis0 · slot-mint"));
+ assert_eq!(get_all(&d, "lane_tag"), vec![b"DI4M2".to_vec()]);
+ assert_eq!(get_all(&d, "min_outputs"), vec![vec![0x04]]);
+ let hashes = get_all(&d, "output_spk_hash");
+ assert_eq!(hashes.len(), 2, "two repeats in the DI4M2 arity");
+ assert_eq!(
+ hex::encode(&hashes[0]),
+ "c90a93233366793d3a3576e9677a7e1f31ff85c80ba999fc5ca5dbaeac73a544",
+ "first repeat pins the shared marketplace output"
+ );
+
+ // GZ4M1 build: one check.
+ let d = reg.decode(0, include_bytes!("../fixtures/slot_mint_gz4m_a.bin"));
+ assert_eq!(d.template, Some("genesis0 · slot-mint"));
+ assert_eq!(get_all(&d, "lane_tag"), vec![b"GZ4M1".to_vec()]);
+ assert_eq!(get_all(&d, "min_outputs"), vec![vec![0x03]]);
+ assert_eq!(get_all(&d, "output_spk_hash").len(), 1);
+
+ // An unseen third arity still matches: splice in another copy of the
+ // repeated block (instructions 102..111 of the DI4M2 build).
+ let (insts, _) = disassemble(di4m);
+ let start = insts[102].offset;
+ let end = insts[111].offset;
+ let mut three = di4m[..end].to_vec();
+ three.extend_from_slice(&di4m[start..end]);
+ three.extend_from_slice(&di4m[end..]);
+ let d = reg.decode(0, &three);
+ assert_eq!(d.template, Some("genesis0 · slot-mint"), "extra repeat must still match");
+ assert_eq!(get_all(&d, "output_spk_hash").len(), 3);
+
+ // A partial copy breaks group divisibility → no template.
+ let mut ragged = di4m[..end].to_vec();
+ ragged.extend_from_slice(&di4m[start..start + 1]); // lone output-index push
+ ragged.extend_from_slice(&di4m[end..]);
+ assert_eq!(reg.decode(0, &ragged).template, None);
+ }
+
+ #[test]
+ fn derive_observed_edge_cases() {
+ let a = include_bytes!("../fixtures/pure_a.bin").as_slice();
+ let b = include_bytes!("../fixtures/pure_b.bin").as_slice();
+ // one instance is not a derivation
+ assert!(Skeleton::derive_observed("x", &[a], &["k"]).is_none());
+ // label count must equal the distinct slots (one here)
+ assert!(Skeleton::derive_observed("x", &[a, b], &[]).is_none());
+ assert!(Skeleton::derive_observed("x", &[a, b], &["k", "extra"]).is_none());
+ assert!(Skeleton::derive_observed("x", &[a, b], &["k"]).is_some());
+ // shape mismatch across instances → None
+ let other = include_bytes!("../fixtures/g0_col_a.bin").as_slice();
+ assert!(Skeleton::derive_observed("x", &[a, other], &["k"]).is_none());
+ // repeat derivation needs both arities
+ assert!(RepeatSkeleton::derive("x", &[a, b], &[], &["k"], &[]).is_none());
+ }
+
+ #[test]
+ fn zk_band_boundaries() {
+ let probe = |n: usize| {
+ let mut s = encode_push(&vec![0x5a; n]);
+ s.push(0xa6); // OpZkPrecompile
+ zk_system(&s)
+ };
+ assert_eq!(probe(127), None);
+ assert_eq!(probe(128), Some("groth16"));
+ assert_eq!(probe(256), Some("groth16"));
+ assert_eq!(probe(257), None, "gap above the Groth16 band stays unattributed");
+ assert_eq!(probe(299), None);
+ assert_eq!(probe(300), Some("succinct proof (inferred)"));
+ assert_eq!(probe(1023), Some("succinct proof (inferred)"));
+ assert_eq!(probe(1024), Some("risc0"));
+ }
+
+ #[test]
+ fn p2sh_reveal_verifies_and_peels() {
+ let redeem = vec![0xb9, 0xcf, 0x51]; // OpTxInputIndex OpInputCovenantId OpTrue
+ let hash = blake2b_simd::Params::new().hash_length(32).hash(&redeem);
+ let mut spk = vec![0xaa, 0x20];
+ spk.extend_from_slice(hash.as_bytes());
+ spk.push(0x87);
+ // sig script: some witness push, then the redeem script push
+ let mut sig = vec![0x02, 0x01, 0x02, 0x03];
+ sig.extend_from_slice(&redeem);
+ assert_eq!(p2sh_reveal(&spk, &sig), Some(redeem.clone()));
+
+ // wrong redeem → hash mismatch → no reveal
+ let mut bad_sig = vec![0x03];
+ bad_sig.extend_from_slice(&[0x51, 0x52, 0x53]);
+ assert_eq!(p2sh_reveal(&spk, &bad_sig), None);
+
+ // non-P2SH spk → no reveal
+ assert_eq!(p2sh_reveal(&[0xac], &sig), None);
+ }
+}
diff --git a/vendor/kascov-preflight/crates/kascov-decode/src/observed.rs b/vendor/kascov-preflight/crates/kascov-decode/src/observed.rs
new file mode 100644
index 0000000..aec4e81
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-decode/src/observed.rs
@@ -0,0 +1,153 @@
+//! Skeletons derived from real revealed programs observed on chain.
+//!
+//! Unlike [`crate::silverscript_skeletons`] (compiler dumps built with
+//! sentinel arguments), these families were learned from spend-time P2SH
+//! reveals in the TN10 index: each fixture pair is two distinct on-chain
+//! instances of the same compiled contract, and the derivation marks the
+//! positions where real deployments disagree as labeled slots
+//! ([`Skeleton::derive_observed`]; [`RepeatSkeleton::derive`] additionally
+//! takes a second arity so the repeated per-output block is matched as a
+//! group). Names follow the protocol tags the covenants themselves put in
+//! their accepted-transaction payloads — the evidence is cited per family.
+//!
+//! Fixture bytes are verbatim reveal programs (`p2sh_reveal` verified them
+//! against the committed state hash before they were captured), stored under
+//! `fixtures/` and embedded at compile time.
+
+use crate::{RepeatSkeleton, Skeleton};
+
+macro_rules! fixture {
+ ($name:literal) => {
+ include_bytes!(concat!("../fixtures/", $name, ".bin")).as_slice()
+ };
+}
+
+/// Fixed-shape families seen on TN10. Every skeleton derives from two
+/// distinct real instances; a family compiled at several arities/branches
+/// registers one skeleton per observed build, all under the same name.
+pub fn observed_skeletons() -> Vec {
+ let mut out = Vec::new();
+ let mut add = |s: Option| out.extend(s);
+
+ // PURE: 14 covenants / ~1.4k spends+burns whose event payloads all read
+ // "PURE\0…". One inlined argument: the key that OpCheckSigFromStack
+ // verifies right after the leading Dup·SHA256 of the witness message.
+ add(Skeleton::derive_observed(
+ "PURE",
+ &[fixture!("pure_a"), fixture!("pure_b")],
+ &["signer_pubkey"],
+ ));
+
+ // genesis0 marketplace listings. A listing covenant is spent twice, and
+ // the accepted-tx payload names the program that ran each time:
+ // {"t":"genesis0-list","v":1,…} for the first spend and
+ // {"t":"genesis0-buy","v":1,…} (or …-delist) for the second — 993+982
+ // covenants of the larger build, 205+188 of the smaller. The "list"
+ // program embeds the byte template of the follow-up "buy" state
+ // (`next_state_template` below literally starts with the buy program's
+ // post-state bytes), which is how the two stages were tied together.
+ add(Skeleton::derive_observed(
+ "genesis0 · list",
+ &[fixture!("g0_list_v1_a"), fixture!("g0_list_v1_b")],
+ &["state_hash_a", "state_hash_b", "state_hash_c", "min_amount", "next_state_template"],
+ ));
+ add(Skeleton::derive_observed(
+ "genesis0 · buy",
+ &[fixture!("g0_buy_v1_a"), fixture!("g0_buy_v1_b")],
+ &[
+ "state_amount",
+ "state_hash_a",
+ "state_hash_b",
+ "min_amount",
+ "output_spk_hash_a",
+ "output_amount",
+ "output_spk_hash_b",
+ ],
+ ));
+ add(Skeleton::derive_observed(
+ "genesis0 · list",
+ &[fixture!("g0_list_v2_a"), fixture!("g0_list_v2_b")],
+ &["witness_hash", "output_spk_hash", "salt_a", "salt_b"],
+ ));
+ add(Skeleton::derive_observed(
+ "genesis0 · buy",
+ &[fixture!("g0_buy_v2_a"), fixture!("g0_buy_v2_b")],
+ &["output_spk_hash", "price", "witness_hash", "salt"],
+ ));
+
+ // genesis0 collection registry: 41 covenants whose spends carry
+ // {"t":"genesis0","v":1,"col":…}. One inlined argument — the amount the
+ // covenant sheds per mint (input amount minus `amount_step` must equal
+ // output 0's amount).
+ add(Skeleton::derive_observed(
+ "genesis0 · collection",
+ &[fixture!("g0_col_a"), fixture!("g0_col_b")],
+ &["amount_step"],
+ ));
+
+ // KCC20 token (kcc20.sil): state rides as the leading
+ // OpToAltStack-guarded pushes and matches the contract's field order —
+ // byte[32] ownerIdentifier, byte identifierType (0x00 pubkey / 0x01
+ // script hash / 0x02 covenant id), int amount, bool isMinter. Three
+ // builds circulate on TN10 (~200 covenants): the compiler unrolls
+ // `maxCovIns`/`maxCovOuts` loops and constant-folds the isMinter branch,
+ // so each build gets its own skeleton under the one name.
+ for f in [
+ [fixture!("kcc20_a_a"), fixture!("kcc20_a_b")],
+ [fixture!("kcc20_b_a"), fixture!("kcc20_b_b")],
+ [fixture!("kcc20_c_a"), fixture!("kcc20_c_b")],
+ ] {
+ add(Skeleton::derive_observed(
+ "KCC20 token",
+ &f,
+ &["owner_identifier", "identifier_type", "amount", "is_minter"],
+ ));
+ }
+
+ // KCC20 minter/controller: pins two covenant ids with OpInputCovenantId
+ // + OpOutputCovenantId (each id is required on the way in *and* out, so
+ // the two uses fold into one slot each) and embeds the KCC20 token
+ // template bytes three times to validate the governed token states it
+ // mints into. Both pinned ids resolve to live "KCC20 token" covenants in
+ // the TN10 index.
+ add(Skeleton::derive_observed(
+ "KCC20 minter",
+ &[fixture!("kcc20_minter_a"), fixture!("kcc20_minter_b")],
+ &["kcc20_covenant_a", "kcc20_covenant_b"],
+ ));
+
+ out
+}
+
+/// Variable-arity families: one skeleton matches every repeat count.
+pub fn observed_repeat_skeletons() -> Vec {
+ let mut out = Vec::new();
+
+ // genesis0 slot-mint — the DI4M/GZ4M lanes' mint contract and by far the
+ // busiest program on TN10 (~8.5k spends, ~40% of all P2SH reveal
+ // traffic). Every spend's payload opens with the 5-byte lane tag
+ // ("DI4M2"/"GZ4M1") followed by {"t":"genesis0-slot-mint","v":2,…}. The
+ // build repeats one `OpTxOutputAmount…OpTxOutputSpk` check per
+ // collection output, so the two observed arities (two checks for DI4M2,
+ // one for GZ4M1) derive a repeat group; arity-dependent constants like
+ // the minimum output count become slots automatically.
+ if let Some(s) = RepeatSkeleton::derive(
+ "genesis0 · slot-mint",
+ &[fixture!("slot_mint_di4m_a"), fixture!("slot_mint_di4m_b")],
+ &[fixture!("slot_mint_gz4m_a"), fixture!("slot_mint_gz4m_b")],
+ &[
+ "min_outputs",
+ "lane_tag",
+ "payload_hash_a",
+ "payload_hash_b",
+ "payload_len_a",
+ "payload_len_b",
+ "instance_salt",
+ ],
+ &["output_index", "output_amount", "output_index", "output_spk_hash"],
+ ) {
+ out.push(s);
+ }
+
+ out
+}
diff --git a/vendor/kascov-preflight/crates/kascov-sim/Cargo.toml b/vendor/kascov-preflight/crates/kascov-sim/Cargo.toml
new file mode 100644
index 0000000..9c164b7
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-sim/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "kascov-sim"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+kascov-decode = { workspace = true }
+kaspa-consensus-core = { workspace = true }
+kaspa-txscript = { workspace = true }
+kaspa-addresses = { workspace = true }
+secp256k1 = { workspace = true }
+blake2b_simd = { workspace = true }
+hex = { workspace = true }
+serde = { workspace = true }
diff --git a/vendor/kascov-preflight/crates/kascov-sim/src/lib.rs b/vendor/kascov-preflight/crates/kascov-sim/src/lib.rs
new file mode 100644
index 0000000..be378e7
--- /dev/null
+++ b/vendor/kascov-preflight/crates/kascov-sim/src/lib.rs
@@ -0,0 +1,670 @@
+//! kascov-sim — run a *hypothetical* covenant spend through Kaspa's real
+//! `TxScriptEngine`, off-chain, with no node and no private keys.
+//!
+//! A live coin commits to someone else's key, so a browser can't produce that
+//! signature. Instead we re-instantiate the same contract with a fresh
+//! STAND-IN signer (swap the authorized-party hash for `blake2b(stand-in pk)`),
+//! fabricate the state UTXO, build the spend the caller describes, sign it with
+//! the stand-in key, and execute the exact node-side script validation. So the
+//! signature rule always resolves, and the *other* rules — amount, destination,
+//! timelock, introspection — are tested for real against the scenario.
+
+use blake2b_simd::Params as Blake2bParams;
+use kaspa_addresses::{Address, Prefix, Version as AddrVersion};
+use kaspa_consensus_core::{
+ constants::TX_VERSION_TOCCATA,
+ hashing::sighash::{calc_schnorr_signature_hash, SigHashReusedValuesUnsync},
+ hashing::sighash_type::SIG_HASH_ALL,
+ mass::units::ComputeBudget,
+ subnets::SUBNETWORK_ID_NATIVE,
+ tx::{
+ ComputeCommit, MutableTransaction, ScriptPublicKey, Transaction, TransactionInput,
+ TransactionOutpoint, TransactionOutput, UtxoEntry,
+ },
+};
+use kaspa_consensus_core::mass::units::ScriptUnits;
+use kaspa_txscript::{
+ caches::Cache, pay_to_address_script, pay_to_script_hash_script, EngineCtx, EngineFlags,
+ TxScriptEngine,
+};
+use secp256k1::{Keypair, SECP256K1};
+use serde::{Deserialize, Serialize};
+
+/// What the caller wants to try.
+#[derive(Debug, Clone, Deserialize)]
+pub struct SimRequest {
+ /// The coin's compiled program (hex).
+ pub program_hex: String,
+ /// Which entrypoint to satisfy: `spend` (Escrow) | `reclaim` | `cold` | `inherit`.
+ pub entrypoint: String,
+ /// Where the funds go: `buyer` | `seller` | `other` | `self`.
+ #[serde(default = "default_recipient")]
+ pub recipient: String,
+ /// The state coin's value, in sompi.
+ #[serde(default = "default_value")]
+ pub value: u64,
+ /// Output-0 value in sompi; default = `value − 1000` (the contract's fee).
+ #[serde(default)]
+ pub amount: Option,
+ /// Capture the concrete per-opcode execution trace (real stacks, real
+ /// control flow) for the visual debugger.
+ #[serde(default)]
+ pub trace: bool,
+}
+
+/// One step of the real engine's execution: the opcode and the stacks as they
+/// stood just before it ran (concrete hex values).
+#[derive(Debug, Clone, Serialize)]
+pub struct TraceStep {
+ pub op: String,
+ pub dstack: Vec,
+ pub astack: Vec,
+}
+
+fn default_recipient() -> String {
+ "self".into()
+}
+fn default_value() -> u64 {
+ 100_000_000
+}
+
+/// The verdict.
+#[derive(Debug, Clone, Serialize)]
+pub struct SimResult {
+ /// Was the request runnable (recognized template + known entrypoint)?
+ pub ok: bool,
+ /// Did the spend satisfy the contract — what a node would decide.
+ pub pass: bool,
+ /// Human-readable verdict (the engine's reason on failure).
+ pub verdict: String,
+ pub template: String,
+ pub entrypoint: String,
+ pub recipient: String,
+ /// The output value used (sompi).
+ pub output_value: u64,
+ /// On failure: the specific contract rule the spend violates (plain English).
+ #[serde(default)]
+ pub rule: String,
+ /// Concrete per-opcode execution trace (only when requested).
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub trace: Vec,
+ /// Honest framing shown in the UI.
+ pub note: String,
+}
+
+impl SimResult {
+ fn err(entrypoint: &str, msg: impl Into) -> Self {
+ SimResult {
+ ok: false,
+ pass: false,
+ verdict: msg.into(),
+ template: String::new(),
+ entrypoint: entrypoint.to_string(),
+ recipient: String::new(),
+ output_value: 0,
+ rule: String::new(),
+ trace: Vec::new(),
+ note: String::new(),
+ }
+ }
+}
+
+/// On a failed spend, name the specific rule the scenario violates. The engine
+/// only reports "verification failed" at the OpVerify, but for a known template
+/// + scenario the offending require is deterministic (and matches the source
+/// order — Escrow checks the amount before the destination).
+fn failing_rule(template: &str, recipient: &str, value: u64, output_value: u64) -> String {
+ match template {
+ "SilverScript · Escrow" => {
+ if output_value != value.saturating_sub(1000) {
+ "the output must equal the escrowed value minus the contract's 1000-sompi fee".into()
+ } else if !matches!(recipient, "buyer" | "seller") {
+ "the escrow can only pay the committed buyer or seller — no third address".into()
+ } else {
+ String::new()
+ }
+ }
+ _ => String::new(),
+ }
+}
+
+fn blake2b32(bytes: &[u8]) -> [u8; 32] {
+ *Blake2bParams::new().hash_length(32).hash(bytes).as_bytes().first_chunk::<32>().unwrap()
+}
+
+fn xonly(kp: &Keypair) -> [u8; 32] {
+ kp.public_key().x_only_public_key().0.serialize()
+}
+
+fn p2pk_spk(xonly_pk: &[u8]) -> Option {
+ let addr = Address::new(Prefix::Testnet, AddrVersion::PubKey, xonly_pk);
+ Some(pay_to_address_script(&addr))
+}
+
+/// (selector to push after pk+sig, the committed hash field the signer must
+/// match). Mirrors kascov-lab's `entrypoint_spec`, plus Escrow.
+fn spec(template: &str, entrypoint: &str) -> Option<(Option, &'static str)> {
+ match (template, entrypoint) {
+ ("SilverScript · Escrow", "spend") => Some((None, "arbiter_hash")),
+ ("SilverScript · Mecenas", "reclaim") => Some((Some(1), "funder_hash")),
+ ("SilverScript · LastWill", "cold") => Some((Some(1), "cold_hash")),
+ ("SilverScript · LastWill", "inherit") => Some((Some(0), "inheritor_hash")),
+ _ => None,
+ }
+}
+
+/// Replace the 32-byte `old` subsequence in `program` with `new` (both 32B).
+/// The authorized-party field is a blake2b hash — unique in the script.
+fn splice_field(program: &[u8], old: &[u8], new: &[u8; 32]) -> Option> {
+ if old.len() != 32 {
+ return None;
+ }
+ let pos = program.windows(32).position(|w| w == old)?;
+ let mut out = program.to_vec();
+ out[pos..pos + 32].copy_from_slice(new);
+ Some(out)
+}
+
+pub fn simulate(req: &SimRequest) -> SimResult {
+ let program = match hex::decode(req.program_hex.trim().trim_start_matches("0x")) {
+ Ok(p) if !p.is_empty() => p,
+ _ => return SimResult::err(&req.entrypoint, "program isn't valid hex"),
+ };
+ let decoded = kascov_decode::Registry::default().decode(0, &program);
+ let Some(template) = decoded.template else {
+ return SimResult::err(&req.entrypoint, "not a recognized SilverScript contract");
+ };
+ let Some((selector, signer_field)) = spec(template, &req.entrypoint) else {
+ return SimResult::err(
+ &req.entrypoint,
+ format!("simulation doesn't support {template} · {}", req.entrypoint),
+ );
+ };
+ let field = |name: &str| decoded.fields.iter().find(|f| f.name == name).map(|f| f.value.clone());
+ let Some(committed) = field(signer_field) else {
+ return SimResult::err(&req.entrypoint, format!("{template} has no {signer_field}"));
+ };
+
+ // A stand-in signer, and the contract re-instantiated to trust it.
+ let stand_in = Keypair::new(SECP256K1, &mut secp256k1::rand::thread_rng());
+ let pk = xonly(&stand_in);
+ let Some(program2) = splice_field(&program, &committed, &blake2b32(&pk)) else {
+ return SimResult::err(&req.entrypoint, "couldn't re-instantiate the contract");
+ };
+
+ // Where the money goes.
+ let other = Keypair::new(SECP256K1, &mut secp256k1::rand::thread_rng());
+ let recipient_pk: Vec = match req.recipient.as_str() {
+ "buyer" => match field("buyer") {
+ Some(v) => v,
+ None => return SimResult::err(&req.entrypoint, "this contract has no buyer"),
+ },
+ "seller" => match field("seller") {
+ Some(v) => v,
+ None => return SimResult::err(&req.entrypoint, "this contract has no seller"),
+ },
+ "other" => xonly(&other).to_vec(),
+ _ => pk.to_vec(), // "self"
+ };
+ let Some(dest_spk) = p2pk_spk(&recipient_pk) else {
+ return SimResult::err(&req.entrypoint, "bad recipient key");
+ };
+
+ let value = req.value.max(1_000_000);
+ let output_value = req.amount.unwrap_or(value.saturating_sub(1000));
+
+ // One input (the fabricated covenant state), one output.
+ let state_spk = pay_to_script_hash_script(&program2);
+ let outpoint = TransactionOutpoint::new(kaspa_consensus_core::Hash::from_bytes([0x11; 32]), 0);
+ let input = TransactionInput::new_with_mass(
+ outpoint,
+ vec![],
+ 0,
+ ComputeCommit::ComputeBudget(ComputeBudget(60)),
+ );
+ let output = TransactionOutput::new(output_value, dest_spk);
+ let tx = Transaction::new(TX_VERSION_TOCCATA, vec![input], vec![output], 0, SUBNETWORK_ID_NATIVE, 0, vec![]);
+ // block_daa_score = 0 → the coin reads as maximally old, so relative
+ // timelocks (reclaim) are satisfied and don't mask the rule under test.
+ let entry = UtxoEntry::new(value, state_spk, 0, false, None);
+ let mut mtx = MutableTransaction::with_entries(tx, vec![entry]);
+
+ // Sign the schnorr sighash over the P2SH UTXO with the stand-in key.
+ let reused = SigHashReusedValuesUnsync::new();
+ let sig_hash = calc_schnorr_signature_hash(&mtx.as_verifiable(), 0, SIG_HASH_ALL, &reused);
+ let msg = match secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()) {
+ Ok(m) => m,
+ Err(_) => return SimResult::err(&req.entrypoint, "sighash error"),
+ };
+ let sig = stand_in.sign_schnorr(msg);
+ let mut sig_arg = sig.as_ref().to_vec();
+ sig_arg.push(SIG_HASH_ALL.to_u8());
+
+ let mut witness = Vec::new();
+ witness.extend_from_slice(&kascov_decode::encode_push(&pk));
+ witness.extend_from_slice(&kascov_decode::encode_push(&sig_arg));
+ if let Some(sel) = selector {
+ witness.extend_from_slice(&kascov_decode::encode_push(&kascov_decode::snum(sel)));
+ }
+ witness.extend_from_slice(&kascov_decode::encode_push(&program2));
+ mtx.tx.inputs[0].signature_script = witness;
+
+ let (pass, verdict, trace) = run_engine(&mtx, req.trace);
+ let rule = if pass { String::new() } else { failing_rule(template, &req.recipient, value, output_value) };
+ SimResult {
+ ok: true,
+ pass,
+ verdict,
+ template: template.to_string(),
+ entrypoint: req.entrypoint.clone(),
+ recipient: req.recipient.clone(),
+ output_value,
+ rule,
+ trace,
+ note: "simulated with a stand-in signer — the signature rule always resolves so the amount, destination & timelock rules are what's tested".into(),
+ }
+}
+
+/// A real on-chain spend replayed through the engine: the verdict plus the
+/// concrete per-opcode trace of the ACTUAL witness running against the ACTUAL
+/// locking script.
+#[derive(Debug, Clone, Serialize)]
+pub struct DebugResult {
+ /// The replay ran (inputs were non-empty and an engine could be built).
+ pub ok: bool,
+ /// Did the replay execute cleanly inside the fabricated context?
+ pub pass: bool,
+ pub verdict: String,
+ /// Per-opcode execution steps (stacks as they stood before each opcode).
+ pub trace: Vec,
+ /// Honest framing of the fabricated-context limitation.
+ pub note: String,
+}
+
+/// Build the fabricated 1-in/1-out replay context `debug_witness` and the
+/// preflight's isolated execution share: the input spends the given state
+/// coin, and output 0 re-locks value−1000 to the SAME state script, bound to
+/// the same covenant when known — the closest generic stand-in for "the state
+/// moves forward one step".
+fn fabricated_replay_tx(
+ spk_version: u16,
+ spk_script: &[u8],
+ sig_script: &[u8],
+ value: u64,
+ budget: u16,
+ covenant_id: Option<[u8; 32]>,
+) -> MutableTransaction {
+ let state_spk = ScriptPublicKey::from_vec(spk_version, spk_script.to_vec());
+ let outpoint = TransactionOutpoint::new(kaspa_consensus_core::Hash::from_bytes([0x11; 32]), 0);
+ let input = TransactionInput::new_with_mass(
+ outpoint,
+ sig_script.to_vec(),
+ 0,
+ ComputeCommit::ComputeBudget(ComputeBudget(budget)),
+ );
+ let cov_hash = covenant_id.map(kaspa_consensus_core::Hash::from_bytes);
+ let output = TransactionOutput::with_covenant(
+ value.saturating_sub(1000),
+ state_spk.clone(),
+ cov_hash.map(|id| kaspa_consensus_core::tx::CovenantBinding::new(0, id)),
+ );
+ let tx = Transaction::new(TX_VERSION_TOCCATA, vec![input], vec![output], 0, SUBNETWORK_ID_NATIVE, 0, vec![]);
+ // block_daa_score = 0 → the coin reads as maximally old, so relative
+ // timelocks don't mask the data flow under inspection.
+ let entry = UtxoEntry::new(value, state_spk, 0, false, cov_hash);
+ MutableTransaction::with_entries(tx, vec![entry])
+}
+
+/// Replay a REAL captured spend — the state coin's locking script
+/// (`spk_version` + `spk_script`), its value, and the on-chain unlocking
+/// script (`sig_script`) — through Kaspa's `TxScriptEngine`, capturing the
+/// per-opcode trace. When the coin's covenant id is known, the fabricated
+/// UTXO carries it and output 0 is bound as a same-covenant continuation, so
+/// covenant introspection opcodes resolve instead of erroring immediately.
+///
+/// LIMITATION: the transaction context is fabricated (one input, one
+/// state-continuation output), not the original tx. Signature checks hash
+/// THIS tx, so an `OpCheckSig` that passed on-chain fails here; introspection
+/// opcodes (output amounts/scripts, covenant bindings) read the fabricated
+/// context and may diverge from the original too. What IS faithful: the
+/// witness data, the revealed program, the P2SH hash check, and every
+/// data/control-flow opcode in between — which is what the visual debugger
+/// walks.
+pub fn debug_witness(
+ spk_version: u16,
+ spk_script: &[u8],
+ sig_script: &[u8],
+ value: u64,
+ budget: Option,
+ covenant_id: Option<[u8; 32]>,
+) -> DebugResult {
+ const NOTE: &str = "replayed against a fabricated 1-in/1-out transaction — the witness, revealed program and data flow are the real on-chain bytes, but signature and introspection checks see this fabricated context, not the original tx, so they can fail here even though the spend passed on-chain";
+ if spk_script.is_empty() || sig_script.is_empty() {
+ return DebugResult {
+ ok: false,
+ pass: false,
+ verdict: "no locking or unlocking script to replay".into(),
+ trace: Vec::new(),
+ note: NOTE.into(),
+ };
+ }
+ // The real budget commitment when captured; otherwise generous, so a
+ // fabricated budget shortfall never masks the rules under test.
+ let mtx = fabricated_replay_tx(spk_version, spk_script, sig_script, value, budget.unwrap_or(u16::MAX), covenant_id);
+ let (pass, verdict, trace, _) = run_engine_at(&mtx, 0, true, None);
+ DebugResult { ok: true, pass, verdict, trace, note: NOTE.into() }
+}
+
+/// One input's preflight execution verdict: did the real engine accept the
+/// witness, and how many script units did it burn against the input's own
+/// committed allowance (budget × 10,000 + the 9,999 free units).
+#[derive(Debug, Clone, Serialize)]
+pub struct InputExec {
+ pub input_index: usize,
+ pub pass: bool,
+ pub verdict: String,
+ pub script_units_used: u64,
+ pub allowance: u64,
+}
+
+/// Execute the listed inputs of a fully-populated transaction through the
+/// real engine, each limited to its OWN committed compute budget — exactly
+/// the per-input allowance a node enforces. The caller guarantees every
+/// entry in `mtx.entries` is populated (the preflight only takes this path
+/// when the submitted JSON carries a utxo for every input).
+pub fn preflight_execute(mtx: &MutableTransaction, indices: &[usize]) -> Vec {
+ indices
+ .iter()
+ .map(|&i| {
+ let limit = mtx.tx.inputs[i].compute_commit.allowed_script_units();
+ let (pass, verdict, _, used) = run_engine_at(mtx, i, false, Some(limit));
+ InputExec { input_index: i, pass, verdict, script_units_used: used, allowance: limit.0 }
+ })
+ .collect()
+}
+
+/// Execute ONE input in the fabricated 1-in/1-out context `debug_witness`
+/// uses (for transactions where not every input carries a utxo, so the real
+/// tx context can't be populated), limited to the input's own budget.
+pub fn preflight_execute_isolated(
+ input_index: usize,
+ spk_version: u16,
+ spk_script: &[u8],
+ sig_script: &[u8],
+ value: u64,
+ budget: u16,
+ covenant_id: Option<[u8; 32]>,
+) -> InputExec {
+ let mtx = fabricated_replay_tx(spk_version, spk_script, sig_script, value, budget, covenant_id);
+ let limit = ComputeCommit::ComputeBudget(ComputeBudget(budget)).allowed_script_units();
+ let (pass, verdict, _, used) = run_engine_at(&mtx, 0, false, Some(limit));
+ InputExec { input_index, pass, verdict, script_units_used: used, allowance: limit.0 }
+}
+
+/// Run a self-contained ZK verification script (public inputs + proof + vk +
+/// OpZkPrecompile) through the real engine — invoking the exact ark_groth16 /
+/// RISC-Zero verification a Kaspa node performs. Returns (valid, reason).
+pub fn verify_zk_script(program: &[u8]) -> (bool, String) {
+ let sig_cache = Cache::new(10);
+ let reused = SigHashReusedValuesUnsync::new();
+ match kaspa_txscript::zk_precompiles::tests::helpers::execute_zk_script(program, &sig_cache, &reused) {
+ Ok(()) => (true, "the zero-knowledge proof VERIFIED — the same on-chain check Kaspa L1 performs".into()),
+ Err(e) => (false, format!("proof rejected: {e}")),
+ }
+}
+
+fn run_engine(mtx: &MutableTransaction, trace: bool) -> (bool, String, Vec) {
+ let (pass, verdict, steps, _) = run_engine_at(mtx, 0, trace, None);
+ (pass, verdict, steps)
+}
+
+/// Run input `idx` of a fully-populated transaction through the engine.
+/// `limit` bounds execution to a real per-input script-unit allowance (what a
+/// node enforces from the committed budget); `None` runs unmetered. Returns
+/// (pass, verdict, trace steps, script units actually consumed).
+fn run_engine_at(
+ mtx: &MutableTransaction,
+ idx: usize,
+ trace: bool,
+ limit: Option,
+) -> (bool, String, Vec, u64) {
+ let reused = SigHashReusedValuesUnsync::new();
+ let vtx = mtx.as_verifiable();
+ let sig_cache = Cache::new(10_000);
+ let entry = mtx.entries[idx].clone().expect("entry present");
+ // The covenant introspection context a node would precompute for this tx
+ // (input/output indices per covenant id) — without it every OpCov* opcode
+ // sees an empty map and errors out immediately.
+ let cov_ctx = match kaspa_txscript::covenants::CovenantsContext::from_tx(&vtx) {
+ Ok(ctx) => ctx,
+ Err(e) => return (false, format!("covenant bindings invalid: {e}"), Vec::new(), 0),
+ };
+ let mut buf: Vec = Vec::new();
+ let (pass, verdict, used) = {
+ let ctx = EngineCtx::new(&sig_cache).with_reused(&reused).with_covenants_ctx(&cov_ctx);
+ let flags = EngineFlags { covenants_enabled: true, ..Default::default() };
+ let mut vm = match limit {
+ Some(limit) => TxScriptEngine::from_transaction_input_with_script_units_limit(
+ &vtx,
+ &mtx.tx.inputs[idx],
+ idx,
+ &entry,
+ ctx,
+ flags,
+ limit,
+ ),
+ None => TxScriptEngine::from_transaction_input(&vtx, &mtx.tx.inputs[idx], idx, &entry, ctx, flags),
+ };
+ if trace {
+ vm = vm.with_opcode_execution_log_buffer(&mut buf);
+ }
+ let outcome = match vm.execute() {
+ Ok(()) => (true, "the spend satisfies the contract — a node would accept it".to_string()),
+ Err(e) => (false, format!("{e}")),
+ };
+ (outcome.0, outcome.1, vm.used_script_units().0)
+ };
+ let steps = if trace { parse_trace(&String::from_utf8_lossy(&buf)) } else { Vec::new() };
+ (pass, verdict, steps, used)
+}
+
+/// Parse the engine's opcode log — each line is
+/// `Executing opcode: , astack: [..], dstack: [..]` with the stacks as they
+/// stood BEFORE that opcode ran.
+fn parse_trace(log: &str) -> Vec {
+ log.lines()
+ .filter_map(|line| {
+ let rest = line.strip_prefix("Executing opcode: ")?;
+ let (op, rest) = rest.split_once(", astack: ")?;
+ let (astack_s, dstack_s) = rest.split_once(", dstack: ")?;
+ Some(TraceStep {
+ op: op.trim().to_string(),
+ astack: parse_hex_array(astack_s),
+ dstack: parse_hex_array(dstack_s),
+ })
+ })
+ .collect()
+}
+
+fn parse_hex_array(s: &str) -> Vec {
+ let s = s.trim().trim_start_matches('[').trim_end_matches(']').trim();
+ if s.is_empty() {
+ return Vec::new();
+ }
+ s.split(',').map(|x| x.trim().trim_matches('"').to_string()).collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ pub const ESCROW: &str = "78aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac6900c2b9be02e803949c6900c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e8700c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e879b69757551";
+
+ fn sim(recipient: &str, amount: Option) -> SimResult {
+ simulate(&SimRequest {
+ program_hex: ESCROW.into(),
+ entrypoint: "spend".into(),
+ recipient: recipient.into(),
+ value: 100_000_000,
+ amount,
+ trace: false,
+ })
+ }
+
+ #[test]
+ fn arbiter_releases_to_buyer_passes() {
+ let r = sim("buyer", None);
+ assert!(r.ok, "runnable: {}", r.verdict);
+ assert!(r.pass, "buyer release should pass, got: {}", r.verdict);
+ }
+
+ #[test]
+ fn arbiter_releases_to_seller_passes() {
+ let r = sim("seller", None);
+ assert!(r.pass, "seller release should pass, got: {}", r.verdict);
+ }
+
+ #[test]
+ fn release_to_third_party_fails() {
+ let r = sim("other", None);
+ assert!(r.ok);
+ assert!(!r.pass, "releasing to a third address must fail");
+ }
+
+ #[test]
+ fn skimming_the_amount_fails() {
+ // send 2000 less than value-1000 → outputs[0].value != value-1000
+ let r = sim("buyer", Some(100_000_000 - 3000));
+ assert!(!r.pass, "skimming must fail the amount rule");
+ }
+
+ #[test]
+ fn unknown_template_is_not_runnable() {
+ let r = simulate(&SimRequest {
+ program_hex: "76a914deadbeef88ac".into(),
+ entrypoint: "spend".into(),
+ recipient: "self".into(),
+ value: 1_000_000,
+ amount: None,
+ trace: false,
+ });
+ assert!(!r.ok);
+ }
+}
+
+#[cfg(test)]
+mod trace_tests {
+ use super::*;
+ #[test]
+ fn concrete_trace_is_captured() {
+ let escrow = tests::ESCROW;
+ let r = simulate(&SimRequest {
+ program_hex: escrow.into(),
+ entrypoint: "spend".into(),
+ recipient: "buyer".into(),
+ value: 100_000_000,
+ amount: None,
+ trace: true,
+ });
+ assert!(r.pass, "buyer release should pass: {}", r.verdict);
+ assert!(!r.trace.is_empty(), "trace should be captured");
+ // stacks are concrete hex; the trace should include real opcodes
+ assert!(r.trace.iter().any(|s| s.op.contains("Op")), "trace has opcodes");
+ eprintln!("trace steps: {}", r.trace.len());
+ eprintln!("first: {:?}", r.trace.first());
+ eprintln!("last: {:?}", r.trace.last());
+ }
+}
+
+#[cfg(test)]
+mod debug_witness_tests {
+ use super::*;
+
+ #[test]
+ fn replays_a_real_p2sh_witness_with_trace() {
+ // A seeded spent p2sh state: program = OpTrue, witness = push(program).
+ // The P2SH hash check and the program itself both run for real.
+ let program = vec![0x51]; // OpTrue
+ let spk = pay_to_script_hash_script(&program);
+ let sig_script = kascov_decode::encode_push(&program);
+ let r = debug_witness(spk.version(), spk.script(), &sig_script, 100_000_000, Some(60), Some([0xAB; 32]));
+ assert!(r.ok, "replay should run: {}", r.verdict);
+ assert!(r.pass, "OpTrue p2sh reveal should pass: {}", r.verdict);
+ assert!(!r.trace.is_empty(), "trace must be captured");
+ assert!(r.trace.iter().any(|s| s.op.contains("Op")), "trace has opcodes");
+ }
+
+ #[test]
+ fn wrong_program_fails_the_p2sh_hash_check() {
+ let program = vec![0x51];
+ let spk = pay_to_script_hash_script(&program);
+ // Reveal a DIFFERENT program than the one committed to.
+ let sig_script = kascov_decode::encode_push(&[0x52]);
+ let r = debug_witness(spk.version(), spk.script(), &sig_script, 100_000_000, None, None);
+ assert!(r.ok);
+ assert!(!r.pass, "a mismatched reveal must fail");
+ }
+
+ #[test]
+ fn empty_inputs_are_not_runnable() {
+ let r = debug_witness(1, &[], &[0x51], 1_000, None, None);
+ assert!(!r.ok);
+ let r = debug_witness(1, &[0x51], &[], 1_000, None, None);
+ assert!(!r.ok);
+ }
+}
+
+#[cfg(test)]
+mod preflight_exec_tests {
+ use super::*;
+
+ #[test]
+ fn isolated_execution_meters_units_against_the_committed_budget() {
+ let program = vec![0x51]; // OpTrue
+ let spk = pay_to_script_hash_script(&program);
+ let sig_script = kascov_decode::encode_push(&program);
+ let r = preflight_execute_isolated(3, spk.version(), spk.script(), &sig_script, 100_000_000, 1, Some([0xAB; 32]));
+ assert_eq!(r.input_index, 3);
+ assert!(r.pass, "OpTrue p2sh reveal should pass: {}", r.verdict);
+ // 1 budget unit × 10,000 script units + the 9,999 free units
+ assert_eq!(r.allowance, 19_999);
+ assert!(r.script_units_used > 0 && r.script_units_used <= r.allowance, "used {} units", r.script_units_used);
+ }
+
+ #[test]
+ fn budget_zero_fails_a_heavy_witness_with_units_exceeded() {
+ // Literal pushes are free, but stack GROWTH is charged per byte —
+ // OpDup on a 12KB item blows straight through the 9,999 free units a
+ // zero-budget input gets.
+ let program = vec![0x76, 0x75, 0x75, 0x51]; // OpDup OpDrop OpDrop OpTrue
+ let spk = pay_to_script_hash_script(&program);
+ let mut sig_script = kascov_decode::encode_push(&vec![0x42u8; 12_000]);
+ sig_script.extend_from_slice(&kascov_decode::encode_push(&program));
+ let fail = preflight_execute_isolated(0, spk.version(), spk.script(), &sig_script, 100_000_000, 0, None);
+ assert!(!fail.pass, "budget 0 must not cover a 12KB witness: {}", fail.verdict);
+ assert_eq!(fail.allowance, 9_999);
+ // The same witness passes once the budget covers it.
+ let pass = preflight_execute_isolated(0, spk.version(), spk.script(), &sig_script, 100_000_000, 3, None);
+ assert!(pass.pass, "budget 3 (30,000 units + free) should cover it: {}", pass.verdict);
+ assert!(pass.script_units_used > 9_999 && pass.script_units_used <= pass.allowance);
+ }
+}
+
+#[cfg(test)]
+mod zk_probe {
+ #[test]
+ fn real_groth16_proof_verifies_through_the_engine() {
+ use kaspa_txscript::caches::Cache;
+ use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync;
+ use kaspa_txscript::zk_precompiles::tests::helpers::{build_groth_script, execute_zk_script};
+ // a complete Groth16-verifying script built from the dep's real fixture
+ let script = build_groth_script();
+ eprintln!("groth script bytes: {}", script.len());
+ eprintln!("groth script hex: {}", script.iter().map(|b| format!("{:02x}", b)).collect::());
+ let sig_cache = Cache::new(10);
+ let reused = SigHashReusedValuesUnsync::new();
+ let r = execute_zk_script(&script, &sig_cache, &reused);
+ eprintln!("verify result: {r:?}");
+ assert!(r.is_ok(), "the real Groth16 proof should verify: {r:?}");
+ }
+}
diff --git a/vendor/kascov-preflight/crates/studio-kascov-preflight/Cargo.toml b/vendor/kascov-preflight/crates/studio-kascov-preflight/Cargo.toml
new file mode 100644
index 0000000..e90063a
--- /dev/null
+++ b/vendor/kascov-preflight/crates/studio-kascov-preflight/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "studio-kascov-preflight"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[features]
+kascov-index-fixture = []
+
+[dependencies]
+blake2b_simd = { workspace = true }
+kascov-decode = { workspace = true }
+kascov-sim = { workspace = true }
+kaspa-consensus-core = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+hex = { workspace = true }
+
+[[bin]]
+name = "kascov-preflight"
+path = "src/main.rs"
diff --git a/native/kascov-preflight-main.rs b/vendor/kascov-preflight/crates/studio-kascov-preflight/src/main.rs
similarity index 100%
rename from native/kascov-preflight-main.rs
rename to vendor/kascov-preflight/crates/studio-kascov-preflight/src/main.rs
diff --git a/vendor/kascov-preflight/crates/studio-kascov-preflight/src/preflight.rs b/vendor/kascov-preflight/crates/studio-kascov-preflight/src/preflight.rs
new file mode 100644
index 0000000..af0307d
--- /dev/null
+++ b/vendor/kascov-preflight/crates/studio-kascov-preflight/src/preflight.rs
@@ -0,0 +1,1347 @@
+//! Transaction preflight — "will this transaction pass?", answered BEFORE
+//! broadcast, with the same primitives a node uses. Feed it the SDK-dict /
+//! RPC JSON of a transaction and it reports the traps covenant builders
+//! actually hit (rusty-kaspa#1073's missing computeBudget, the forgotten fee
+//! input, the block-mass ceiling), computes the real consensus masses, and —
+//! when inputs carry their witness and utxo — runs them through Kaspa's
+//! actual script engine metered against each input's own committed budget.
+//!
+//! Pure computation: no node, no keys, no state. The handler in main.rs adds
+//! the rate limiter and the body cap; everything here is testable offline.
+
+use kaspa_consensus_core::config::params::{Params, MAINNET_PARAMS, TESTNET_PARAMS};
+use kaspa_consensus_core::mass::units::{ComputeBudget, ScriptUnits};
+use kaspa_consensus_core::mass::{calc_storage_mass, MassCalculator, UtxoCell};
+use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE;
+use kaspa_consensus_core::tx::{
+ ComputeCommit, CovenantBinding, MutableTransaction, ScriptPublicKey, Transaction,
+ TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry,
+};
+use crate::Network;
+
+/// Consensus caps (Params::max_tx_inputs/max_tx_outputs) — anything past them
+/// is un-relayable regardless of what the fields say.
+const MAX_TX_IO: usize = 1000;
+/// Ceiling on engine executions per request — execution burns real CPU and
+/// the findings past the first dozen inputs repeat themselves anyway.
+const EXEC_INPUT_CAP: usize = 16;
+
+fn params_for(network: Network) -> &'static Params {
+ match network {
+ Network::Mainnet => &MAINNET_PARAMS,
+ // testnet-10 params; other testnet suffixes share them.
+ Network::Testnet(_) => &TESTNET_PARAMS,
+ }
+}
+
+/// One diagnosis. `code` is the stable machine key the frontend switches on;
+/// `message` lifts the guide's trap prose so the tool and the guide speak the
+/// same language.
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct Finding {
+ pub severity: &'static str, // "error" | "warn" | "info"
+ pub code: &'static str,
+ pub message: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub input_index: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub suggestion: Option,
+}
+
+/// A parsed input — everything optional, because the whole point is telling
+/// the caller what's missing instead of failing on it.
+#[derive(Default)]
+struct PInput {
+ outpoint_txid: Option<[u8; 32]>,
+ outpoint_index: u32,
+ sequence: u64,
+ sig_op_count: Option,
+ /// The budget as the node will see it (camelCase field only — a
+ /// snake_case `compute_budget` is what pre-Toccata SDK serializers drop,
+ /// so it deliberately does NOT populate this).
+ compute_budget: Option,
+ signature_script: Option>,
+ utxo_amount: Option,
+ utxo_spk: Option<(u16, Vec)>,
+ utxo_covenant_id: Option<[u8; 32]>,
+}
+
+#[derive(Default)]
+struct POutput {
+ value: Option,
+ spk: Option<(u16, Vec)>,
+ covenant: Option<([u8; 32], u16)>, // (covenant id, authorizing input)
+}
+
+/// Everything the checks need, pulled out of the JSON with findings recorded
+/// along the way.
+struct PTx {
+ version: Option,
+ inputs: Vec,
+ outputs: Vec,
+ lock_time: u64,
+ payload: Vec,
+}
+
+// ── tolerant JSON access ───────────────────────────────────────────────────
+
+/// camelCase → snake_case ("computeBudget" → "compute_budget").
+fn snake_of(camel: &str) -> String {
+ let mut out = String::with_capacity(camel.len() + 4);
+ for c in camel.chars() {
+ if c.is_ascii_uppercase() {
+ out.push('_');
+ out.push(c.to_ascii_lowercase());
+ } else {
+ out.push(c);
+ }
+ }
+ out
+}
+
+/// Integer that may arrive as a JSON number or a decimal string (u64 doesn't
+/// round-trip through every SDK's JSON layer, so strings are common).
+fn as_u64(v: &serde_json::Value) -> Option {
+ match v {
+ serde_json::Value::Number(n) => n.as_u64(),
+ serde_json::Value::String(s) => s.trim().parse().ok(),
+ _ => None,
+ }
+}
+
+fn as_hex(v: &serde_json::Value) -> Option> {
+ let s = v.as_str()?.trim().trim_start_matches("0x");
+ hex::decode(s).ok()
+}
+
+fn as_hex32(v: &serde_json::Value) -> Option<[u8; 32]> {
+ as_hex(v)?.try_into().ok()
+}
+
+/// Field lookup: camelCase first, then the snake_case spelling. The caller
+/// learns which spelling matched, so snake_case is DETECTED, never silently
+/// normalized away.
+fn field<'a>(obj: &'a serde_json::Map, camel: &str) -> (Option<&'a serde_json::Value>, bool) {
+ if let Some(v) = obj.get(camel) {
+ return (Some(v), false);
+ }
+ let snake = snake_of(camel);
+ if snake != camel {
+ if let Some(v) = obj.get(&snake) {
+ return (Some(v), true);
+ }
+ }
+ (None, false)
+}
+
+/// Collector for parse-time findings.
+struct ParseLog {
+ findings: Vec,
+ /// snake_case fields already reported (one finding per field name).
+ snake_seen: Vec,
+ /// unknown keys seen anywhere (deduped, order kept).
+ unknown: Vec,
+}
+
+impl ParseLog {
+ fn new() -> Self {
+ Self { findings: Vec::new(), snake_seen: Vec::new(), unknown: Vec::new() }
+ }
+
+ fn snake(&mut self, camel: &str, input_index: Option) {
+ let name = snake_of(camel);
+ if self.snake_seen.contains(&name) {
+ return;
+ }
+ self.snake_seen.push(name.clone());
+ // compute_budget is THE proven footgun (rusty-kaspa#1073 wears it):
+ // SDKs pinned to pre-Toccata wire formats (wasm/py 2.0.x) silently
+ // drop the snake_case field when serializing, the node sees budget 0
+ // and answers limit=9999 no matter what the local object said.
+ if name == "compute_budget" {
+ self.findings.push(Finding {
+ severity: "warn",
+ code: "snake_case_field",
+ message: "`compute_budget` is spelled snake_case — SDK serializers pinned to pre-Toccata wire \
+ formats silently drop the field, so the node sees budget 0 and answers `limit=9999` \
+ no matter what your local object said. This preflight mirrors the node and ignores \
+ it too; spell it `computeBudget` in Toccata-aware tooling."
+ .into(),
+ input_index,
+ suggestion: Some("rename compute_budget → computeBudget".into()),
+ });
+ } else {
+ self.findings.push(Finding {
+ severity: "info",
+ code: "snake_case_field",
+ message: format!(
+ "`{name}` is spelled snake_case — accepted here, but Toccata-era SDK dictionaries expect \
+ camelCase (`{camel}`); serializers frozen pre-Toccata may drop it"
+ ),
+ input_index,
+ suggestion: None,
+ });
+ }
+ }
+
+ fn unknown_keys(&mut self, obj: &serde_json::Map, known: &[&str]) {
+ for key in obj.keys() {
+ let matches_known =
+ known.iter().any(|k| key.as_str() == *k || key.as_str() == snake_of(k));
+ if !matches_known && !self.unknown.contains(key) {
+ self.unknown.push(key.clone());
+ }
+ }
+ }
+}
+
+fn parse_spk(v: &serde_json::Value, log: &mut ParseLog, input_index: Option) -> Option<(u16, Vec)> {
+ match v {
+ serde_json::Value::String(_) => as_hex(v).map(|script| (0u16, script)),
+ serde_json::Value::Object(obj) => {
+ log.unknown_keys(obj, &["version", "script", "scriptPublicKey", "hex"]);
+ let (version, vsnake) = field(obj, "version");
+ if vsnake {
+ log.snake("version", input_index);
+ }
+ let version = version.and_then(as_u64).unwrap_or(0) as u16;
+ let script = ["script", "scriptPublicKey", "hex"]
+ .iter()
+ .find_map(|k| {
+ let (v, snake) = field(obj, k);
+ if snake {
+ log.snake(k, input_index);
+ }
+ v.and_then(as_hex)
+ })?;
+ Some((version, script))
+ }
+ _ => None,
+ }
+}
+
+/// Parse the submitted JSON into a PTx + parse findings. `Err` is reserved
+/// for bodies we can't analyze at all (the handler's 400).
+fn parse(body: &str, log: &mut ParseLog) -> Result {
+ let root: serde_json::Value =
+ serde_json::from_str(body).map_err(|e| format!("not valid JSON: {e}"))?;
+ let mut obj = root.as_object().ok_or("expected a JSON object describing a transaction")?;
+ // Tolerate the RPC submit wrapper: {"transaction": {...}, "allowOrphan": …}.
+ if let Some(inner) = obj.get("transaction").and_then(|v| v.as_object()) {
+ obj = inner;
+ }
+ log.unknown_keys(
+ obj,
+ &["version", "inputs", "outputs", "lockTime", "subnetworkId", "gas", "payload", "mass", "id", "transaction", "allowOrphan", "verboseData"],
+ );
+
+ let (version, snake) = field(obj, "version");
+ if snake {
+ log.snake("version", None);
+ }
+ let version = version.and_then(as_u64);
+
+ let (lock_time, snake) = field(obj, "lockTime");
+ if snake {
+ log.snake("lockTime", None);
+ }
+ let lock_time = lock_time.and_then(as_u64).unwrap_or(0);
+
+ let (payload, snake) = field(obj, "payload");
+ if snake {
+ log.snake("payload", None);
+ }
+ let payload = payload.and_then(as_hex).unwrap_or_default();
+
+ let (inputs_v, snake) = field(obj, "inputs");
+ if snake {
+ log.snake("inputs", None);
+ }
+ let inputs_v = inputs_v.and_then(|v| v.as_array()).cloned().unwrap_or_default();
+ let (outputs_v, snake) = field(obj, "outputs");
+ if snake {
+ log.snake("outputs", None);
+ }
+ let outputs_v = outputs_v.and_then(|v| v.as_array()).cloned().unwrap_or_default();
+
+ let mut inputs = Vec::with_capacity(inputs_v.len().min(MAX_TX_IO));
+ for (i, iv) in inputs_v.iter().enumerate().take(MAX_TX_IO) {
+ let mut input = PInput::default();
+ let Some(iobj) = iv.as_object() else {
+ inputs.push(input);
+ continue;
+ };
+ log.unknown_keys(
+ iobj,
+ &["previousOutpoint", "sequence", "sigOpCount", "computeBudget", "signatureScript", "utxo", "utxoEntry", "verboseData"],
+ );
+
+ let (outpoint, snake) = field(iobj, "previousOutpoint");
+ if snake {
+ log.snake("previousOutpoint", Some(i));
+ }
+ if let Some(oobj) = outpoint.and_then(|v| v.as_object()) {
+ log.unknown_keys(oobj, &["transactionId", "index"]);
+ let (txid, snake) = field(oobj, "transactionId");
+ if snake {
+ log.snake("transactionId", Some(i));
+ }
+ input.outpoint_txid = txid.and_then(as_hex32);
+ let (index, snake) = field(oobj, "index");
+ if snake {
+ log.snake("index", Some(i));
+ }
+ input.outpoint_index = index.and_then(as_u64).unwrap_or(0) as u32;
+ }
+
+ let (sequence, snake) = field(iobj, "sequence");
+ if snake {
+ log.snake("sequence", Some(i));
+ }
+ input.sequence = sequence.and_then(as_u64).unwrap_or(0);
+
+ let (sigops, snake) = field(iobj, "sigOpCount");
+ if snake {
+ log.snake("sigOpCount", Some(i));
+ }
+ input.sig_op_count = sigops.and_then(as_u64);
+
+ // camelCase ONLY populates the budget; snake_case is detected and
+ // deliberately ignored — exactly what a pre-Toccata serializer does.
+ if let Some(v) = iobj.get("computeBudget") {
+ input.compute_budget = as_u64(v);
+ if input.compute_budget.is_none() {
+ log.findings.push(Finding {
+ severity: "warn",
+ code: "bad_number",
+ message: format!("input {i}: computeBudget isn't a readable integer — treating it as absent"),
+ input_index: Some(i),
+ suggestion: None,
+ });
+ }
+ } else if iobj.contains_key("compute_budget") {
+ log.snake("computeBudget", Some(i));
+ }
+
+ let (sig, snake) = field(iobj, "signatureScript");
+ if snake {
+ log.snake("signatureScript", Some(i));
+ }
+ if let Some(v) = sig {
+ input.signature_script = as_hex(v);
+ if input.signature_script.is_none() && !v.is_null() {
+ log.findings.push(Finding {
+ severity: "warn",
+ code: "bad_hex",
+ message: format!("input {i}: signatureScript isn't valid hex — treating it as absent"),
+ input_index: Some(i),
+ suggestion: None,
+ });
+ }
+ }
+
+ let utxo = ["utxo", "utxoEntry"].iter().find_map(|k| {
+ let (v, snake) = field(iobj, k);
+ if snake {
+ log.snake(k, Some(i));
+ }
+ v.and_then(|v| v.as_object())
+ });
+ if let Some(uobj) = utxo {
+ log.unknown_keys(uobj, &["amount", "value", "scriptPublicKey", "blockDaaScore", "isCoinbase", "covenantId"]);
+ input.utxo_amount = ["amount", "value"].iter().find_map(|k| {
+ let (v, snake) = field(uobj, k);
+ if snake {
+ log.snake(k, Some(i));
+ }
+ v.and_then(as_u64)
+ });
+ let (spk, snake) = field(uobj, "scriptPublicKey");
+ if snake {
+ log.snake("scriptPublicKey", Some(i));
+ }
+ input.utxo_spk = spk.and_then(|v| parse_spk(v, log, Some(i)));
+ let (cov, snake) = field(uobj, "covenantId");
+ if snake {
+ log.snake("covenantId", Some(i));
+ }
+ input.utxo_covenant_id = cov.and_then(as_hex32);
+ }
+ inputs.push(input);
+ }
+
+ let mut outputs = Vec::with_capacity(outputs_v.len().min(MAX_TX_IO));
+ for (i, ov) in outputs_v.iter().enumerate().take(MAX_TX_IO) {
+ let mut output = POutput::default();
+ let Some(oobj) = ov.as_object() else {
+ outputs.push(output);
+ continue;
+ };
+ log.unknown_keys(oobj, &["value", "amount", "scriptPublicKey", "covenant", "verboseData"]);
+ output.value = ["value", "amount"].iter().find_map(|k| {
+ let (v, snake) = field(oobj, k);
+ if snake {
+ log.snake(k, None);
+ }
+ v.and_then(as_u64)
+ });
+ let (spk, snake) = field(oobj, "scriptPublicKey");
+ if snake {
+ log.snake("scriptPublicKey", None);
+ }
+ output.spk = spk.and_then(|v| parse_spk(v, log, None));
+ let (cov, snake) = field(oobj, "covenant");
+ if snake {
+ log.snake("covenant", None);
+ }
+ if let Some(cobj) = cov.and_then(|v| v.as_object()) {
+ log.unknown_keys(cobj, &["covenantId", "id", "authorizingInput"]);
+ let id = ["covenantId", "id"].iter().find_map(|k| {
+ let (v, snake) = field(cobj, k);
+ if snake {
+ log.snake(k, None);
+ }
+ v.and_then(as_hex32)
+ });
+ let (auth, snake) = field(cobj, "authorizingInput");
+ if snake {
+ log.snake("authorizingInput", None);
+ }
+ let auth = auth.and_then(as_u64).unwrap_or(0) as u16;
+ output.covenant = id.map(|id| (id, auth));
+ }
+ let _ = i;
+ outputs.push(output);
+ }
+
+ if inputs_v.len() > MAX_TX_IO || outputs_v.len() > MAX_TX_IO {
+ log.findings.push(Finding {
+ severity: "error",
+ code: "too_many_io",
+ message: format!(
+ "{} inputs / {} outputs — consensus caps both at {MAX_TX_IO}; only the first {MAX_TX_IO} were analyzed",
+ inputs_v.len(),
+ outputs_v.len()
+ ),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+
+ Ok(PTx { version, inputs, outputs, lock_time, payload })
+}
+
+// ── static checks ──────────────────────────────────────────────────────────
+
+/// Does this input look like a covenant spend (as opposed to the plain
+/// fee/change input everybody forgets)? P2SH-shaped utxo lock, or a witness
+/// big enough to be revealing a program rather than pushing pk+signature.
+fn covenant_looking(input: &PInput) -> bool {
+ if let Some((_, script)) = &input.utxo_spk {
+ if kascov_decode::p2sh_hash(script).is_some() {
+ return true;
+ }
+ }
+ input.signature_script.as_ref().is_some_and(|s| s.len() > 150)
+}
+
+/// The smallest computeBudget whose allowance covers `used` script units —
+/// the consensus rounding (free allowance included), not naive division.
+pub fn covering_budget(used: u64) -> Option {
+ ComputeBudget::checked_covering_script_units(ScriptUnits(used)).map(|b| b.0)
+}
+
+/// Run the preflight over a request body. `Err` means the body wasn't
+/// analyzable at all (not JSON / not an object) → the handler's 400.
+pub fn run(body: &str, network: Network) -> Result {
+ let mut log = ParseLog::new();
+ let ptx = parse(body, &mut log)?;
+ let mut findings = std::mem::take(&mut log.findings);
+ let params = params_for(network);
+
+ if !log.unknown.is_empty() {
+ let shown: Vec<&str> = log.unknown.iter().take(5).map(|s| s.as_str()).collect();
+ findings.push(Finding {
+ severity: "info",
+ code: "unknown_fields",
+ message: format!(
+ "{} unrecognized field{} ignored ({}{})",
+ log.unknown.len(),
+ if log.unknown.len() == 1 { "" } else { "s" },
+ shown.join(", "),
+ if log.unknown.len() > 5 { ", …" } else { "" },
+ ),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+
+ let version = match ptx.version {
+ Some(v) => v,
+ None => {
+ findings.push(Finding {
+ severity: "warn",
+ code: "version_missing",
+ message: "no version field — assuming version 1 (Toccata); pre-Toccata transactions must say version 0".into(),
+ input_index: None,
+ suggestion: Some("set version: 1".into()),
+ });
+ 1
+ }
+ };
+ let version_known = version <= 1;
+ if !version_known {
+ findings.push(Finding {
+ severity: "warn",
+ code: "version_unknown",
+ message: format!("version {version} isn't a Kaspa transaction version this tool knows (0 = legacy, 1 = Toccata) — static checks were skipped"),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+
+ if ptx.inputs.is_empty() {
+ findings.push(Finding {
+ severity: "error",
+ code: "no_inputs",
+ message: "the transaction has no inputs — nothing to spend".into(),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+ if ptx.outputs.is_empty() {
+ findings.push(Finding {
+ severity: "error",
+ code: "no_outputs",
+ message: "the transaction has no outputs — a valid transaction pays somewhere".into(),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+
+ // Execution first (when possible): measured script units make the budget
+ // suggestions exact instead of heuristic.
+ let executable: Vec = ptx
+ .inputs
+ .iter()
+ .enumerate()
+ .filter(|(_, inp)| {
+ inp.signature_script.as_ref().is_some_and(|s| !s.is_empty())
+ && inp.utxo_spk.as_ref().is_some_and(|(_, s)| !s.is_empty())
+ && inp.utxo_amount.is_some()
+ })
+ .map(|(i, _)| i)
+ .take(EXEC_INPUT_CAP)
+ .collect();
+
+ // Full-transaction context needs a buildable tx: every input populated
+ // with a utxo, and every input's compute commitment expressible.
+ let all_populated = !ptx.inputs.is_empty()
+ && ptx
+ .inputs
+ .iter()
+ .all(|inp| inp.utxo_spk.is_some() && inp.utxo_amount.is_some());
+
+ // Per-input budget traps (v1 only — v0 inputs commit sigOpCount).
+ // `budgets_ok` = every v1 input's commitment is expressible on the wire,
+ // the precondition for the mass calculator (it panics otherwise).
+ let mut budgets_ok = true;
+ if version == 1 {
+ for (i, input) in ptx.inputs.iter().enumerate() {
+ let budget = input.compute_budget;
+ if budget.is_none() {
+ budgets_ok = false;
+ }
+ if let Some(b) = budget {
+ if b > u16::MAX as u64 {
+ budgets_ok = false;
+ findings.push(Finding {
+ severity: "error",
+ code: "budget_overflow",
+ message: format!("input {i}: computeBudget {b} doesn't fit the u16 wire field (max 65,535)"),
+ input_index: Some(i),
+ suggestion: None,
+ });
+ continue;
+ }
+ if input.sig_op_count.is_some_and(|s| s > 0) {
+ findings.push(Finding {
+ severity: "info",
+ code: "sigop_count_ignored",
+ message: format!(
+ "input {i}: sigOpCount isn't part of the version-1 wire format — serializers drop it; the committed computeBudget is what counts"
+ ),
+ input_index: Some(i),
+ suggestion: None,
+ });
+ }
+ }
+ if budget.is_none() && input.sig_op_count.is_some_and(|s| s > 0) {
+ // The migration finding: a v0-shaped input pasted into a v1 tx.
+ findings.push(Finding {
+ severity: "error",
+ code: "sigop_count_on_v1",
+ message: format!(
+ "input {i} still carries the v0 per-input sigOpCount — version-1 (Toccata) transactions replace it with a computeBudget: u16 commitment (1 budget unit = 100 grams = 10,000 script units). Without it the node grants only the free 9,999 units."
+ ),
+ input_index: Some(i),
+ suggestion: Some("replace sigOpCount with computeBudget (10 covers one CheckSig)".into()),
+ });
+ continue;
+ }
+ if budget.is_none() || budget == Some(0) {
+ // Evidence beats heuristic: if this input executes cleanly
+ // under budget 0, the free 9,999 units genuinely cover it.
+ let measured = executable.contains(&i).then(|| {
+ let (spk_v, spk) = input.utxo_spk.clone().expect("executable implies utxo");
+ kascov_sim::preflight_execute_isolated(
+ i,
+ spk_v,
+ &spk,
+ input.signature_script.as_deref().unwrap_or_default(),
+ input.utxo_amount.unwrap_or(0),
+ u16::MAX,
+ input.utxo_covenant_id,
+ )
+ });
+ if let Some(probe) = &measured {
+ if probe.pass && probe.script_units_used <= 9_999 {
+ continue; // budget 0 honestly suffices for this input
+ }
+ }
+ let suggestion = match &measured {
+ Some(probe) if probe.pass => covering_budget(probe.script_units_used)
+ .map(|b| format!("set computeBudget: {} — this witness measured {} script units", b.max(1), probe.script_units_used)),
+ _ if covenant_looking(input) => Some(
+ "set computeBudget: 20 (kascov-lab's default per input), or measure the exact need with a dry-run".into(),
+ ),
+ _ => Some("set computeBudget: 10 — one Schnorr CheckSig costs exactly 100,000 script units (10 units)".into()),
+ };
+ if covenant_looking(input) {
+ findings.push(Finding {
+ severity: "error",
+ code: "budget_missing",
+ message: format!(
+ "input {i} commits {} computeBudget — its effective allowance is the free 9,999 script units, and one Schnorr CheckSig alone costs 100,000 (the `used=100000, limit=9999` of rusty-kaspa#1073). The budget is committed by the spending input and covered by the signature hash — set it BEFORE signing; it can't be patched in afterwards.",
+ if budget.is_none() { "no" } else { "a zero" },
+ ),
+ input_index: Some(i),
+ suggestion,
+ });
+ } else {
+ findings.push(Finding {
+ severity: "error",
+ code: "fee_input_budget_missing",
+ message: format!(
+ "input {i} looks like the plain fee/change input — the one everybody forgets because it \"never needed anything\" pre-Toccata. EVERY input of a version-1 transaction commits its own budget, and this one's CheckSig needs ≥ 10.",
+ ),
+ input_index: Some(i),
+ suggestion,
+ });
+ }
+ }
+ }
+ } else if version == 0 {
+ for (i, input) in ptx.inputs.iter().enumerate() {
+ if input.compute_budget.is_some() {
+ findings.push(Finding {
+ severity: "warn",
+ code: "budget_on_v0",
+ message: format!(
+ "input {i} sets computeBudget on a version-0 transaction — v0 inputs commit sigOpCount, so the field is ignored on the wire. Covenant (Toccata) features need version: 1."
+ ),
+ input_index: Some(i),
+ suggestion: Some("set version: 1 to commit compute budgets".into()),
+ });
+ }
+ if input.sig_op_count.is_some_and(|s| s > u8::MAX as u64) {
+ findings.push(Finding {
+ severity: "error",
+ code: "sigop_overflow",
+ message: format!("input {i}: sigOpCount {} doesn't fit the u8 wire field", input.sig_op_count.unwrap_or(0)),
+ input_index: Some(i),
+ suggestion: None,
+ });
+ }
+ }
+ }
+
+ // Output sanity (needed for both consensus and the storage-mass math).
+ let mut outputs_ok = true;
+ for (i, output) in ptx.outputs.iter().enumerate() {
+ if output.value.is_none() || output.value == Some(0) {
+ outputs_ok = false;
+ findings.push(Finding {
+ severity: "error",
+ code: "output_value_zero",
+ message: format!("output {i} has no (or zero) value — Kaspa outputs must carry a positive amount (KIP-9 storage mass divides by it)"),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+ if output.spk.as_ref().is_none_or(|(_, s)| s.is_empty()) {
+ outputs_ok = false;
+ findings.push(Finding {
+ severity: "error",
+ code: "output_spk_missing",
+ message: format!("output {i} has no scriptPublicKey — nothing would be able to spend it, and the node rejects it"),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+ }
+
+ // ── consensus masses ──────────────────────────────────────────────────
+ // GUARD: MassCalculator::calc_non_contextual_masses panics on a v1 input
+ // without a budget commitment ("v1 transactions are expected to have
+ // compute budget"), so it only runs when every commitment is expressible.
+ let mut masses_json = None;
+ let mut fee_json = None;
+ let mut masses_under_limit = false;
+ let buildable = version_known
+ && !ptx.inputs.is_empty()
+ && !ptx.outputs.is_empty()
+ && (version == 0 || budgets_ok);
+ let tx = buildable.then(|| build_tx(&ptx, version as u16));
+ if let Some(tx) = &tx {
+ let calc = MassCalculator::new_with_consensus_params(params);
+ let nc = calc.calc_non_contextual_masses(tx);
+ let limits = params.block_mass_limits().after();
+ let mut over = Vec::new();
+ if nc.compute_mass > limits.compute {
+ over.push(format!("compute {} > {}", nc.compute_mass, limits.compute));
+ }
+ if nc.transient_mass > limits.transient {
+ over.push(format!("transient {} > {}", nc.transient_mass, limits.transient));
+ }
+
+ // Storage mass needs every input amount (KIP-9 reads both sides).
+ let mut storage: Option = None;
+ let amounts_known = ptx.inputs.iter().all(|i| i.utxo_amount.is_some_and(|a| a > 0));
+ if amounts_known && outputs_ok {
+ let input_cells: Vec = ptx
+ .inputs
+ .iter()
+ .map(|i| match &i.utxo_spk {
+ Some((v, s)) => (&UtxoEntry::new(
+ i.utxo_amount.unwrap_or(1),
+ ScriptPublicKey::from_vec(*v, s.clone()),
+ 0,
+ false,
+ None,
+ ))
+ .into(),
+ None => UtxoCell::new(1, i.utxo_amount.unwrap_or(1)),
+ })
+ .collect();
+ let output_cells = tx.outputs.iter().map(UtxoCell::from);
+ match calc_storage_mass(false, input_cells.iter().copied(), output_cells, params.storage_mass_parameter) {
+ Some(mass) => {
+ if mass > limits.storage {
+ over.push(format!("storage {} > {}", mass, limits.storage));
+ }
+ storage = Some(mass);
+ }
+ None => {
+ findings.push(Finding {
+ severity: "error",
+ code: "storage_mass_incomputable",
+ message: "storage mass overflows — the outputs split the value too finely for the inputs (KIP-9); consolidate outputs or raise their amounts".into(),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+ }
+ } else if outputs_ok {
+ findings.push(Finding {
+ severity: "info",
+ code: "storage_mass_skipped",
+ message: "storage mass wasn't checked — include each input's utxo amount to check the KIP-9 side too".into(),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+
+ if !over.is_empty() {
+ findings.push(Finding {
+ severity: "error",
+ code: "mass_exceeds_limit",
+ message: format!(
+ "mass exceeds the per-block ceiling ({}) — the transaction is un-spendable on-chain; no fee fixes it. Commit smaller budgets or split the transaction.",
+ over.join(", ")
+ ),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+ masses_under_limit = over.is_empty();
+ masses_json = Some(serde_json::json!({
+ "compute": nc.compute_mass,
+ "transient": nc.transient_mass,
+ "storage": storage,
+ "limit": { "compute": limits.compute, "transient": limits.transient, "storage": limits.storage },
+ }));
+ // The fee family kascov-lab ships for its spends (100 sompi per gram
+ // of compute mass + fixed headroom) — labeled an estimate because
+ // that's what it is: Kaspa has no consensus minimum-fee primitive.
+ fee_json = Some(serde_json::json!({
+ "estimate_sompi": 100u64.saturating_mul(nc.compute_mass).saturating_add(200_000),
+ "note": "estimate from kascov-lab's shipped fee formula (100 sompi × compute grams + 200,000 headroom) — Kaspa has no consensus minimum fee; mempools set their own floors",
+ }));
+ } else if version_known && !ptx.inputs.is_empty() && !ptx.outputs.is_empty() {
+ findings.push(Finding {
+ severity: "info",
+ code: "mass_skipped",
+ message: "consensus masses weren't computed — every input of a version-1 transaction must commit a computeBudget first (see the findings above)".into(),
+ input_index: None,
+ suggestion: None,
+ });
+ }
+
+ // ── engine execution ──────────────────────────────────────────────────
+ let mut executed: Vec = Vec::new();
+ let mut execution_note = None;
+ if !executable.is_empty() {
+ if let (Some(tx), true) = (&tx, all_populated) {
+ // Real context: the engine sees the declared outputs, sequences
+ // and covenant bindings, so introspection AND signatures are
+ // faithful — what a node validates, minus chain context.
+ let entries: Vec = ptx
+ .inputs
+ .iter()
+ .map(|i| {
+ let (v, s) = i.utxo_spk.clone().expect("all_populated");
+ UtxoEntry::new(
+ i.utxo_amount.expect("all_populated"),
+ ScriptPublicKey::from_vec(v, s),
+ 0,
+ false,
+ i.utxo_covenant_id.map(kaspa_consensus_core::Hash::from_bytes),
+ )
+ })
+ .collect();
+ let mtx = MutableTransaction::with_entries(tx.clone(), entries);
+ executed = kascov_sim::preflight_execute(&mtx, &executable);
+ execution_note = Some(
+ "executed against the transaction as submitted — outputs, sequences and covenant bindings are the real ones, so signature and introspection checks are faithful",
+ );
+ } else {
+ // Isolated fallback: each input replayed in a fabricated
+ // 1-in/1-out continuation (same context the tx debugger uses).
+ for &i in &executable {
+ let input = &ptx.inputs[i];
+ let (spk_v, spk) = input.utxo_spk.clone().expect("executable implies utxo");
+ executed.push(kascov_sim::preflight_execute_isolated(
+ i,
+ spk_v,
+ &spk,
+ input.signature_script.as_deref().unwrap_or_default(),
+ input.utxo_amount.unwrap_or(0),
+ input.compute_budget.unwrap_or(0).min(u16::MAX as u64) as u16,
+ input.utxo_covenant_id,
+ ));
+ }
+ execution_note = Some(
+ "executed in an isolated per-input context (not every input carried a utxo, so the full transaction couldn't be populated) — signature and introspection checks may diverge from a real validation",
+ );
+ }
+ for exec in &executed {
+ if !exec.pass {
+ let units_exceeded = exec.verdict.contains("script units exceeded");
+ findings.push(Finding {
+ severity: "error",
+ code: "input_script_failed",
+ message: format!("input {}: the script engine rejected the witness — {}", exec.input_index, exec.verdict),
+ input_index: Some(exec.input_index),
+ suggestion: units_exceeded
+ .then(|| "raise this input's computeBudget to cover its measured script units".to_string()),
+ });
+ }
+ }
+ }
+
+ // ── verdict ───────────────────────────────────────────────────────────
+ let has_error = findings.iter().any(|f| f.severity == "error");
+ let verdict = if has_error {
+ "will_fail"
+ } else if masses_json.is_some() && masses_under_limit {
+ "ready"
+ } else {
+ "incomplete"
+ };
+
+ Ok(serde_json::json!({
+ "ok": true,
+ "network": network.to_string(),
+ "verdict": verdict,
+ "findings": findings,
+ "masses": masses_json,
+ "fee": fee_json,
+ "executed": if executed.is_empty() { None } else { Some(&executed) },
+ "execution_note": execution_note,
+ "note": "static + engine preflight over the transaction as submitted — chain context (utxo existence, maturity, fee market) still belongs to the node",
+ }))
+}
+
+/// Build the consensus Transaction the mass calculator and the engine share.
+/// Only called when `buildable` holds (v1 inputs all carry budgets).
+fn build_tx(ptx: &PTx, version: u16) -> Transaction {
+ let inputs = ptx
+ .inputs
+ .iter()
+ .map(|i| {
+ let commit = if version >= 1 {
+ ComputeCommit::ComputeBudget(ComputeBudget(i.compute_budget.unwrap_or(0).min(u16::MAX as u64) as u16))
+ } else {
+ // Plain spends carry one CheckSig; assume it when unstated.
+ ComputeCommit::SigopCount((i.sig_op_count.unwrap_or(1).min(u8::MAX as u64) as u8).into())
+ };
+ TransactionInput {
+ previous_outpoint: TransactionOutpoint::new(
+ kaspa_consensus_core::Hash::from_bytes(i.outpoint_txid.unwrap_or([0; 32])),
+ i.outpoint_index,
+ ),
+ signature_script: i.signature_script.clone().unwrap_or_default(),
+ sequence: i.sequence,
+ compute_commit: commit,
+ }
+ })
+ .collect();
+ let outputs = ptx
+ .outputs
+ .iter()
+ .map(|o| {
+ TransactionOutput::with_covenant(
+ o.value.unwrap_or(1),
+ o.spk
+ .clone()
+ .map(|(v, s)| ScriptPublicKey::from_vec(v, s))
+ .unwrap_or_else(|| ScriptPublicKey::from_vec(0, Vec::new())),
+ o.covenant
+ .map(|(id, auth)| CovenantBinding::new(auth, kaspa_consensus_core::Hash::from_bytes(id))),
+ )
+ })
+ .collect();
+ Transaction::new(version, inputs, outputs, ptx.lock_time, SUBNETWORK_ID_NATIVE, 0, ptx.payload.clone())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const TN10: Network = Network::Testnet(10);
+
+ /// P2SH lock of `program` (OpBlake2b <32-byte hash> OpEqual, version 0) —
+ /// the same shape kaspa_txscript::pay_to_script_hash_script builds.
+ fn p2sh_spk(program: &[u8]) -> Vec {
+ let hash = blake2b_simd::Params::new().hash_length(32).hash(program);
+ let mut spk = vec![0xaa, 0x20];
+ spk.extend_from_slice(hash.as_bytes());
+ spk.push(0x87);
+ spk
+ }
+
+ fn run_tn10(body: &str) -> serde_json::Value {
+ run(body, TN10).expect("analyzable body")
+ }
+
+ fn codes(v: &serde_json::Value) -> Vec {
+ v["findings"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|f| f["code"].as_str().unwrap().to_string())
+ .collect()
+ }
+
+ fn finding<'a>(v: &'a serde_json::Value, code: &str) -> &'a serde_json::Value {
+ v["findings"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .find(|f| f["code"] == code)
+ .unwrap_or_else(|| panic!("expected finding {code}, got {:?}", codes(v)))
+ }
+
+ /// A well-formed v1 transaction: both inputs budgeted, amounts known.
+ fn good_v1() -> String {
+ serde_json::json!({
+ "version": 1,
+ "inputs": [
+ {
+ "previousOutpoint": { "transactionId": "11".repeat(32), "index": 0 },
+ "sequence": 0,
+ "computeBudget": 20,
+ "utxo": { "amount": 1_000_000_000u64, "scriptPublicKey": { "version": 0, "script": "aa20".to_string() + &"22".repeat(32) + "87" } }
+ },
+ {
+ "previousOutpoint": { "transactionId": "33".repeat(32), "index": 1 },
+ "sequence": 0,
+ "computeBudget": 10,
+ "utxo": { "amount": 500_000_000u64, "scriptPublicKey": { "version": 0, "script": "20".to_string() + &"44".repeat(32) + "ac" } }
+ }
+ ],
+ "outputs": [
+ { "value": 999_999_000u64, "scriptPublicKey": { "version": 0, "script": "20".to_string() + &"55".repeat(32) + "ac" } },
+ { "value": 499_000_000u64, "scriptPublicKey": { "version": 0, "script": "20".to_string() + &"66".repeat(32) + "ac" } }
+ ],
+ "lockTime": 0
+ })
+ .to_string()
+ }
+
+ // ── parse fixtures ─────────────────────────────────────────────────
+
+ #[test]
+ fn camel_case_parses_clean_and_ready() {
+ let v = run_tn10(&good_v1());
+ assert_eq!(v["ok"], true);
+ assert_eq!(v["verdict"], "ready", "findings: {:?}", codes(&v));
+ assert!(v["masses"]["compute"].as_u64().unwrap() > 0);
+ assert!(v["masses"]["storage"].as_u64().is_some());
+ assert!(v["fee"]["estimate_sompi"].as_u64().unwrap() > 0);
+ assert!(!codes(&v).contains(&"snake_case_field".to_string()));
+ }
+
+ #[test]
+ fn snake_case_compute_budget_is_detected_not_silently_accepted() {
+ let body = good_v1().replace("\"computeBudget\":20", "\"compute_budget\":20");
+ let v = run_tn10(&body);
+ let snake = finding(&v, "snake_case_field");
+ assert_eq!(snake["severity"], "warn");
+ assert!(snake["message"].as_str().unwrap().contains("limit=9999"));
+ // …and the node-visible consequence fires too: that input has budget 0.
+ assert!(codes(&v).contains(&"budget_missing".to_string()));
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ #[test]
+ fn string_ints_are_tolerated() {
+ let body = serde_json::json!({
+ "version": "1",
+ "inputs": [{ "computeBudget": "20", "sequence": "0",
+ "utxo": { "amount": "1000000000", "scriptPublicKey": { "version": "0", "script": "20".to_string() + &"44".repeat(32) + "ac" } } }],
+ "outputs": [{ "value": "999999000", "scriptPublicKey": "20".to_string() + &"55".repeat(32) + "ac" }],
+ })
+ .to_string();
+ let v = run_tn10(&body);
+ assert_eq!(v["verdict"], "ready", "findings: {:?}", codes(&v));
+ assert_eq!(v["masses"]["storage"].as_u64().is_some(), true);
+ }
+
+ #[test]
+ fn garbage_is_a_clean_error_not_a_panic() {
+ assert!(run("not json at all {{{", TN10).is_err());
+ assert!(run("[1,2,3]", TN10).is_err());
+ assert!(run("\"just a string\"", TN10).is_err());
+ // an object with nothing useful still analyzes (as incomplete)
+ let v = run_tn10("{}");
+ assert_eq!(v["verdict"], "will_fail"); // no inputs, no outputs
+ assert!(codes(&v).contains(&"no_inputs".to_string()));
+ assert!(codes(&v).contains(&"no_outputs".to_string()));
+ }
+
+ #[test]
+ fn rpc_submit_wrapper_is_unwrapped() {
+ let body = format!("{{\"transaction\": {}, \"allowOrphan\": false}}", good_v1());
+ let v = run_tn10(&body);
+ assert_eq!(v["verdict"], "ready", "findings: {:?}", codes(&v));
+ }
+
+ #[test]
+ fn unknown_fields_are_counted_not_rejected() {
+ let body = good_v1().replacen("\"version\":1", "\"version\":1,\"frobnicator\":7", 1);
+ let v = run_tn10(&body);
+ assert!(finding(&v, "unknown_fields")["message"].as_str().unwrap().contains("frobnicator"));
+ assert_eq!(v["verdict"], "ready");
+ }
+
+ // ── one test per trap ──────────────────────────────────────────────
+
+ #[test]
+ fn trap_missing_budget_on_covenant_input() {
+ let body = good_v1().replacen("\"computeBudget\":20,", "", 1);
+ let v = run_tn10(&body);
+ let f = finding(&v, "budget_missing");
+ assert_eq!(f["severity"], "error");
+ assert_eq!(f["input_index"], 0);
+ let msg = f["message"].as_str().unwrap();
+ assert!(msg.contains("9,999") && msg.contains("100,000"), "trap numbers must be spelled out: {msg}");
+ assert!(f["suggestion"].as_str().unwrap().contains("computeBudget"));
+ assert_eq!(v["verdict"], "will_fail");
+ // the guard: masses must be absent, not a panic
+ assert!(v["masses"].is_null());
+ assert!(codes(&v).contains(&"mass_skipped".to_string()));
+ }
+
+ #[test]
+ fn trap_fee_input_budget_reminder() {
+ // input 1 is the plain p2pk fee input; drop only ITS budget
+ let body = good_v1().replacen("\"computeBudget\":10,", "", 1);
+ let v = run_tn10(&body);
+ let f = finding(&v, "fee_input_budget_missing");
+ assert_eq!(f["input_index"], 1);
+ assert!(f["message"].as_str().unwrap().contains("fee/change input"));
+ assert!(f["suggestion"].as_str().unwrap().contains("computeBudget: 10"));
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ #[test]
+ fn trap_budget_zero_fires_too() {
+ let body = good_v1().replacen("\"computeBudget\":20", "\"computeBudget\":0", 1);
+ let v = run_tn10(&body);
+ assert_eq!(finding(&v, "budget_missing")["input_index"], 0);
+ // zero budgets ARE expressible, so masses still compute (no panic path)
+ assert!(v["masses"]["compute"].as_u64().unwrap() > 0);
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ #[test]
+ fn trap_budget_on_v0_is_flagged_ignored() {
+ let body = good_v1().replacen("\"version\":1", "\"version\":0", 1);
+ let v = run_tn10(&body);
+ let f = finding(&v, "budget_on_v0");
+ assert_eq!(f["severity"], "warn");
+ assert!(f["suggestion"].as_str().unwrap().contains("version: 1"));
+ }
+
+ #[test]
+ fn trap_sigop_count_on_v1_is_the_migration_finding() {
+ let body = good_v1().replacen("\"computeBudget\":20,", "\"sigOpCount\":1,", 1);
+ let v = run_tn10(&body);
+ let f = finding(&v, "sigop_count_on_v1");
+ assert_eq!(f["severity"], "error");
+ assert!(f["message"].as_str().unwrap().contains("10,000 script units"));
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ #[test]
+ fn trap_mass_ceiling_makes_it_unspendable() {
+ // 65,535 budget units = 6,553,500 grams of compute mass — 13× the
+ // 500,000 block ceiling. Committed mass is charged whether the script
+ // uses it or not (the guide's "commit generously, but not 65535").
+ let body = good_v1().replacen("\"computeBudget\":20", "\"computeBudget\":65535", 1);
+ let v = run_tn10(&body);
+ let f = finding(&v, "mass_exceeds_limit");
+ assert!(f["message"].as_str().unwrap().contains("un-spendable"));
+ assert!(v["masses"]["compute"].as_u64().unwrap() > 500_000);
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ #[test]
+ fn trap_zero_value_output() {
+ let body = good_v1().replacen("\"value\":999999000", "\"value\":0", 1);
+ let v = run_tn10(&body);
+ assert_eq!(finding(&v, "output_value_zero")["severity"], "error");
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ #[test]
+ fn storage_mass_needs_input_amounts() {
+ let body = good_v1().replace("\"amount\":1000000000,", "").replace("\"amount\":500000000,", "");
+ let v = run_tn10(&body);
+ assert!(codes(&v).contains(&"storage_mass_skipped".to_string()));
+ assert!(v["masses"]["storage"].is_null());
+ // compute mass still computed and clean → ready
+ assert_eq!(v["verdict"], "ready", "findings: {:?}", codes(&v));
+ }
+
+ // ── covering-budget suggestion math ────────────────────────────────
+
+ #[test]
+ fn covering_budget_respects_the_free_allowance_boundaries() {
+ assert_eq!(covering_budget(0), Some(0));
+ assert_eq!(covering_budget(9_999), Some(0)); // the free allowance
+ assert_eq!(covering_budget(10_000), Some(1));
+ assert_eq!(covering_budget(19_999), Some(1));
+ assert_eq!(covering_budget(20_000), Some(2));
+ assert_eq!(covering_budget(100_000), Some(10)); // one CheckSig
+ assert_eq!(covering_budget(u64::MAX), None); // beyond u16 budgets
+ }
+
+ // ── engine execution ───────────────────────────────────────────────
+
+ /// A synthetic-but-real execution: OpTrue behind a P2SH commitment, the
+ /// exact reveal shape covenants use. Full tx context (utxo on every
+ /// input) → the engine sees the declared outputs.
+ #[test]
+ fn execution_passes_a_clean_p2sh_reveal() {
+ let program = vec![0x51u8]; // OpTrue
+ let spk = p2sh_spk(&program);
+ let witness = kascov_decode::encode_push(&program);
+ let body = serde_json::json!({
+ "version": 1,
+ "inputs": [{
+ "previousOutpoint": { "transactionId": "11".repeat(32), "index": 0 },
+ "sequence": 0,
+ "computeBudget": 1,
+ "signatureScript": hex::encode(&witness),
+ "utxo": { "amount": 100_000_000u64, "scriptPublicKey": { "version": 0, "script": hex::encode(&spk) } }
+ }],
+ "outputs": [{ "value": 99_999_000u64, "scriptPublicKey": { "version": 0, "script": "20".to_string() + &"55".repeat(32) + "ac" } }],
+ })
+ .to_string();
+ let v = run_tn10(&body);
+ let exec = &v["executed"][0];
+ assert_eq!(exec["pass"], true, "verdict: {}", exec["verdict"]);
+ assert_eq!(exec["input_index"], 0);
+ assert_eq!(exec["allowance"], 19_999); // budget 1 × 10,000 + 9,999 free
+ assert!(exec["script_units_used"].as_u64().unwrap() <= 19_999);
+ assert_eq!(v["verdict"], "ready", "findings: {:?}", codes(&v));
+ assert!(v["execution_note"].as_str().unwrap().contains("transaction as submitted"));
+ }
+
+ /// The REAL bytes of an accepted testnet-10 covenant spend (a terminal
+ /// SilverScript · Mecenas `receive`: output-constrained, no signature —
+ /// exactly the witness class whose validity survives replay). Captured
+ /// from the production index; refresh with the ignored helper below if
+ /// testnet-10 resets.
+ mod real_witness {
+ /// P2SH lock of the state coin (spk version 0).
+ pub const STATE_SPK: &str = "aa20693bc1d2d058eae1ca60ed0050f8fbcd724652f6a3e51b7976d8bb4d480231f887";
+ /// The on-chain unlocking script of the spend.
+ pub const WITNESS: &str = "004cb56b6c76009c6375025802b100c320366db7e0f3350cfd60638e0c631061d8d8fac72600ee5e7e258d1fc939b28675030000207c7e01ac7e876902e803b9be760480f0fa0294527994760480f0fa02547993a16300c252795479949c696700c20480f0fa029c6951c3b9bf876951c2789c6968007a75007a75007a75516776519c637578aa20e3777a271a8e60c379317a67b9e5978d82542ed6805192b9558be8e1006649fe8769765279ac69757551677500696868";
+ /// The state coin's value in sompi.
+ pub const VALUE: u64 = 99_998_000;
+ /// The spending input's committed budget (as accepted on-chain).
+ pub const BUDGET: u16 = 20;
+ /// The input sequence the spend carried (the contract's age gate
+ /// compiles to OpCheckSequenceVerify over this field).
+ pub const SEQUENCE: u64 = 600;
+ /// Where the money went: recipient p2pk, value − the contract's 1000.
+ pub const OUT_SPK: &str = "20366db7e0f3350cfd60638e0c631061d8d8fac72600ee5e7e258d1fc939b28675ac";
+ pub const OUT_VALUE: u64 = 99_997_000;
+ }
+
+ #[test]
+ fn execution_passes_a_real_captured_witness() {
+ let body = serde_json::json!({
+ "version": 1,
+ "inputs": [{
+ "previousOutpoint": { "transactionId": "11".repeat(32), "index": 0 },
+ "sequence": real_witness::SEQUENCE,
+ "computeBudget": real_witness::BUDGET,
+ "signatureScript": real_witness::WITNESS,
+ "utxo": { "amount": real_witness::VALUE, "scriptPublicKey": { "version": 0, "script": real_witness::STATE_SPK } }
+ }],
+ "outputs": [{ "value": real_witness::OUT_VALUE, "scriptPublicKey": { "version": 0, "script": real_witness::OUT_SPK } }],
+ })
+ .to_string();
+ let v = run_tn10(&body);
+ let exec = &v["executed"][0];
+ assert_eq!(exec["pass"], true, "the real accepted witness must replay clean: {}", exec["verdict"]);
+ let used = exec["script_units_used"].as_u64().unwrap();
+ assert!(used > 0 && used <= exec["allowance"].as_u64().unwrap());
+ assert_eq!(v["verdict"], "ready", "findings: {:?}", codes(&v));
+ }
+
+ #[test]
+ fn execution_failure_is_a_finding_and_fails_the_verdict() {
+ // Reveal a program that doesn't match the P2SH commitment.
+ let spk = p2sh_spk(&[0x51]);
+ let witness = kascov_decode::encode_push(&[0x52]); // wrong program
+ let body = serde_json::json!({
+ "version": 1,
+ "inputs": [{
+ "computeBudget": 1,
+ "signatureScript": hex::encode(&witness),
+ "utxo": { "amount": 100_000_000u64, "scriptPublicKey": { "version": 0, "script": hex::encode(&spk) } }
+ }],
+ "outputs": [{ "value": 99_999_000u64, "scriptPublicKey": { "version": 0, "script": "20".to_string() + &"55".repeat(32) + "ac" } }],
+ })
+ .to_string();
+ let v = run_tn10(&body);
+ assert_eq!(v["executed"][0]["pass"], false);
+ assert!(codes(&v).contains(&"input_script_failed".to_string()));
+ assert_eq!(v["verdict"], "will_fail");
+ }
+
+ /// Fixture refresher: walks a testnet-10 index copy for a terminal
+ /// Mecenas `receive` whose replay passes, and prints the constants for
+ /// `real_witness` above. Run by hand when testnet-10 resets:
+ /// `KASCOV_FIXTURE_DB=/path/to/testnet-10.db cargo test -p kascov extract_real_witness_fixture -- --ignored --nocapture`
+ #[test]
+ #[ignore]
+ #[cfg(feature = "kascov-index-fixture")]
+ fn extract_real_witness_fixture() {
+ let Ok(db) = std::env::var("KASCOV_FIXTURE_DB") else {
+ eprintln!("set KASCOV_FIXTURE_DB to a testnet-10 index copy");
+ return;
+ };
+ let store = kascov_core::store::Store::open(std::path::Path::new(&db), TN10).unwrap();
+ let registry = kascov_decode::Registry::default();
+ for summary in store.covenants_with_templates(&["SilverScript · Mecenas"]).unwrap() {
+ for utxo in store.utxos(&summary.covenant_id, false).unwrap() {
+ let Some(sig) = utxo.spent_sig.clone().filter(|s| !s.is_empty()) else { continue };
+ let spk = utxo.spk_script.clone();
+ let Some(program) = kascov_decode::p2sh_reveal(&spk, &sig) else { continue };
+ let decoded = registry.decode(0, &program);
+ let field = |n: &str| decoded.fields.iter().find(|f| f.name == n).map(|f| f.value.clone());
+ let (Some(recipient), Some(pledge), Some(period)) = (field("recipient"), field("pledge"), field("period")) else {
+ continue;
+ };
+ let pledge = i64::from_le_bytes({
+ let mut b = [0u8; 8];
+ b[..pledge.len().min(8)].copy_from_slice(&pledge[..pledge.len().min(8)]);
+ b
+ }) as u64;
+ let period = i64::from_le_bytes({
+ let mut b = [0u8; 8];
+ b[..period.len().min(8)].copy_from_slice(&period[..period.len().min(8)]);
+ b
+ }) as u64;
+ // terminal receive: everything − 1000 to the recipient
+ if utxo.value as i128 - pledge as i128 - 1000 > pledge as i128 + 1000 {
+ continue;
+ }
+ let mut out_spk = vec![0x20];
+ out_spk.extend_from_slice(&recipient);
+ out_spk.push(0xac);
+ let exec = kascov_sim::preflight_execute(
+ &{
+ let ptx = PTx {
+ version: Some(1),
+ inputs: vec![PInput {
+ sequence: period,
+ compute_budget: Some(utxo.spent_budget.unwrap_or(20) as u64),
+ signature_script: Some(sig.clone()),
+ utxo_amount: Some(utxo.value),
+ utxo_spk: Some((utxo.spk_version, spk.clone())),
+ ..Default::default()
+ }],
+ outputs: vec![POutput {
+ value: Some(utxo.value - 1000),
+ spk: Some((0, out_spk.clone())),
+ covenant: None,
+ }],
+ lock_time: 0,
+ payload: Vec::new(),
+ };
+ let tx = build_tx(&ptx, 1);
+ let entries = vec![UtxoEntry::new(
+ utxo.value,
+ ScriptPublicKey::from_vec(utxo.spk_version, spk.clone()),
+ 0,
+ false,
+ None,
+ )];
+ MutableTransaction::with_entries(tx, entries)
+ },
+ &[0],
+ );
+ if exec[0].pass {
+ println!("STATE_SPK: {}", hex::encode(&spk));
+ println!("WITNESS: {}", hex::encode(&sig));
+ println!("VALUE: {}", utxo.value);
+ println!("BUDGET: {}", utxo.spent_budget.unwrap_or(20));
+ println!("SEQUENCE: {period}");
+ println!("OUT_SPK: {}", hex::encode(&out_spk));
+ println!("OUT_VALUE: {}", utxo.value - 1000);
+ return;
+ }
+ eprintln!("candidate {} failed: {}", summary.covenant_id, exec[0].verdict);
+ }
+ }
+ panic!("no passing terminal Mecenas receive found in {db}");
+ }
+}