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}"); + } +}