From 88fbd1599257fc0ea0b5f48a56cf23dc4f0f689d Mon Sep 17 00:00:00 2001 From: w00c00 <46839097+w00c00@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:52:02 +0800 Subject: [PATCH] release: Kaspa SilverScript Studio 0.2.8 --- .github/workflows/release-desktop.yml | 1 + CHANGELOG.md | 18 +++ README.md | 14 +- config/compiler-profiles.json | 41 +++-- docs/kcc721-experimental.md | 4 +- docs/portable-covenant-package.md | 25 +++- docs/releases/v0.2.8.md | 47 ++++++ docs/studio-0.2-architecture.md | 20 ++- docs/x402-experimental-profile.md | 48 ++++++ .../references/official-baseline.md | 19 ++- .../references/upstream.json | 4 +- package-lock.json | 4 +- package.json | 2 +- scripts/build-silverc.mjs | 25 +++- scripts/prepare-desktop-runtime.mjs | 2 +- server/atomic-covenant-builder.mjs | 31 +++- server/compiler.mjs | 48 +++++- server/config.mjs | 14 +- server/covenant-descriptor.mjs | 140 ++++++++++++++++++ server/external-covenant-service.mjs | 40 ++++- server/project-store.mjs | 2 +- server/template-operation-service.mjs | 44 +++++- server/template-store.mjs | 12 ++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 11 +- src-tauri/tauri.conf.json | 2 +- src/main.js | 24 ++- templates/commit-reveal/manifest.json | 6 +- templates/groth16-proof-release/contract.sil | 35 +++++ templates/groth16-proof-release/manifest.json | 61 ++++++++ templates/hashlock-refund/manifest.json | 4 + templates/inheritance-vault/manifest.json | 4 + templates/kcc721-experimental/manifest.json | 2 +- templates/merkle-one-time-claim/manifest.json | 6 +- templates/owner-vault/manifest.json | 3 + templates/timelock-transfer/manifest.json | 4 + templates/two-of-three/manifest.json | 3 + test/studio.test.mjs | 51 +++++-- 39 files changed, 741 insertions(+), 84 deletions(-) create mode 100644 docs/releases/v0.2.8.md create mode 100644 docs/x402-experimental-profile.md create mode 100644 server/covenant-descriptor.mjs create mode 100644 templates/groth16-proof-release/contract.sil create mode 100644 templates/groth16-proof-release/manifest.json diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index bb29a32..9e8a17c 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -113,6 +113,7 @@ jobs: PORT=4310 \ STUDIO_DATA_DIR="$data_dir" \ SILVERC_LATEST_BIN="$runtime/bin/silverc-latest" \ + SILVERC_PREVIOUS_BIN="$runtime/bin/silverc-cb34aa5" \ SILVERC_LEGACY_BIN="$runtime/bin/silverc-legacy" \ KASCOV_PREFLIGHT_BIN="$runtime/bin/kascov-preflight" \ "$node_bin" "$runtime/server/index.mjs" >"$log" 2>&1 & diff --git a/CHANGELOG.md b/CHANGELOG.md index 3217944..6e0e40b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.2.8 — 2026-08-10 + +- Updated the default SHA-256-pinned official SilverScript compiler to `6f9e078b1d8b5389212755183b592704de99fea5`; retained `cb34aa5` for Studio 0.2.7 reproducibility and `2a3961c` for older projects. +- Added breaking-change findings for explicit scalar byte/integer conversions: runtime integers use checked `as byte`, while scalar bytes require an explicit `signed()` or `unsigned()` interpretation. +- Added canonical v1 covenant descriptors to generated lifecycle and atomic `.ssinvite` packages, binding CAIP-2 network, program hash, covenant ID, ABI, state layout, and authorization principals. Legacy packages remain readable with a visible warning. +- Added a complete TN10 Experimental Groth16 proof-release template with deterministic parameters, exact fixed-recipient value conservation, bounded fees, an operation builder, local preflight, bilingual UX, and mainnet fail-closed policy. +- Added a pinned Kaspa x402 alpha interoperability profile and explicit admission gates instead of exposing its pre-current-syntax escrow as a deployable template. +- Recompiled every built-in template against `6f9e078` and expanded regression coverage for compiler migration, descriptor tampering, CAIP-2 aliases, Groth16 package construction, and legacy-package compatibility. + +## 0.2.8 — 2026-08-10(中文) + +- 默认官方 SilverScript 编译器升级并固定到 `6f9e078b1d8b5389212755183b592704de99fea5`;保留 `cb34aa5` 复现 Studio 0.2.7 项目,并保留 `2a3961c` 复现更早项目。 +- 增加标量 byte/int 显式转换的破坏性变更提示:运行时整数使用带检查的 `as byte`,标量 byte 转整数必须明确选择 `signed()` 或 `unsigned()`。 +- 新生成的生命周期与原子 `.ssinvite` 操作包加入 canonical v1 Covenant Descriptor,绑定 CAIP-2 网络、程序哈希、Covenant ID、ABI、状态布局和授权主体;旧包仍可读取,但会醒目标出缺少描述符。 +- 增加完整的 TN10 Experimental Groth16 证明释放模板,包含确定性参数、固定收款方精确价值守恒、手续费上限、操作构建器、本地预检、中英双语界面和主网失效关闭策略。 +- 增加固定提交的 Kaspa x402 alpha 互操作档案与正式模板准入条件,没有把仍使用旧语法的上游托管合约直接伪装成可部署模板。 +- 使用 `6f9e078` 重新编译全部内置模板,并扩展编译迁移、描述符篡改、CAIP-2 别名、Groth16 操作包和旧包兼容回归测试。 + ## 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. diff --git a/README.md b/README.md index 7445b98..71a11a3 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 - 中英文界面;首次启动自动读取系统语言,并在系统语言既非中文也非英文时使用时区辅助判断。 - 用户手动切换语言后,本机选择优先于自动识别。 - 本地项目工作区,可创建、切换和删除未使用的工作。 -- 双编译器兼容档案:默认固定 `kaspanet/silverscript@cb34aa5e6a598f9e461c4ad7014279ba89251d8d`,并保留 `2a3961c` 旧版用于复现;两者都校验二进制 SHA-256。 -- 内置破坏性变更扫描与安全迁移,识别 `entry`、`checkMsgSig`、`outpointTxId`、artifact `bytecode` 等升级差异;无法安全自动迁移的 `.reverse()` 和位运算会要求人工审查。 +- 三编译器兼容档案:默认固定 `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5`,保留 Studio 0.2.7 使用的 `cb34aa5` 和 `2a3961c` 旧版用于复现;三个二进制都校验 SHA-256。 +- 内置破坏性变更扫描与安全迁移,识别 `entry`、`checkMsgSig`、`outpointTxId`、artifact `bytecode` 和显式标量 byte/int 转换;无法安全自动迁移的 signed/unsigned 语义、`.reverse()` 和位运算会要求人工审查。 - 固定 Kascov 来源提交构建的本地交易预检引擎。 - 支持 OpenAI、Anthropic、Gemini、OpenRouter、Ollama 和 OpenAI-compatible 接口。 - AI API Key 使用 scrypt 派生密钥和 AES-256-GCM 加密保存在本机。 @@ -45,6 +45,7 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 - 支持 BIP39 附加密码;钱包密码和附加密码不会保存为普通偏好设置。 - 支持 TN10 和 mainnet 自建 wRPC 节点,留空时使用公共节点发现。 - 支持 `.ssinvite` 可携带操作包、跨设备顺序签名和外部 Covenant 交易包审查。 +- 新操作包携带 canonical v1 Covenant Descriptor,绑定 CAIP-2 网络、程序哈希、Covenant ID、ABI、状态布局和授权主体;旧包仍可读取但会标记缺少描述符。 - 可替换 `CovenantStateSource` 会在原生 Covenant RPC、outpoint RPC 与 P2SH 地址索引之间回退,并重新验证 outpoint、Covenant ID、脚本和金额。 - 通用 P2PK co-spend 授权只签指定普通钱包输入,并锁定整笔交易承诺;原子构建器支持 2–32 个不同 Covenant 输入。 - Kascov 是首选可视化和第二份报告来源,但不是签名、预检或广播的运行依赖。 @@ -60,6 +61,7 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 | 多继承人签到金库 | 所有者签到、所有者取回、到期分配 | 多继承人资产安排和定期续期 | | Merkle 一次性领取(TN10 Experimental) | Merkle 证明领取、超时退款 | 白名单领取和一次性票据 | | Commit / Reveal(TN10 Experimental) | Reveal 领取、超时退款 | 域隔离承诺和密封交付 | +| Groth16 证明释放(TN10 Experimental) | ZK 证明领取 | 可验证计算结果付款;收款钱包固定 | | KCC721 四契约包(TN10 Experimental) | Collection、Ticket、NFT、Migration | Covenant 原生 NFT 研究;禁止普通单合约部署 | 每个模板都包含: @@ -223,6 +225,7 @@ OLLAMA_MODEL= 仅有 Covenant ID 或 cov hash 不足以签名。外部操作包必须携带待签交易、UTXO、redeem program、ABI、入口、参数、输出和签名槽信息。详见 [可携带 Covenant 操作包](docs/portable-covenant-package.md)。 编译器升级、状态查询与原子授权接口见 [Studio 0.2 架构说明](docs/studio-0.2-architecture.md)。KCC721 包的来源、边界和禁止事项见 [TN10 Experimental KCC721](docs/kcc721-experimental.md)。 +Kaspa x402 的网络标识、操作包映射和正式模板准入条件见 [TN10 Experimental x402 档案](docs/x402-experimental-profile.md)。 ### 网络 @@ -290,8 +293,8 @@ AI is limited to candidate generation and review assistance. It cannot unlock wa - Chinese and English UI with automatic system-language detection and time-zone fallback. - A manual language choice always overrides future automatic detection. - Local project workspace with explicit create, switch, and delete actions. -- Dual compiler profiles: the default is pinned to `kaspanet/silverscript@cb34aa5e6a598f9e461c4ad7014279ba89251d8d`, while `2a3961c` remains available for reproducible legacy builds; both binaries are SHA-256 verified. -- Built-in breaking-change detection and safe migration for `entry`, `checkMsgSig`, `outpointTxId`, and artifact `bytecode`; removed `.reverse()` and bitwise typing changes require manual review. +- Three compiler profiles: the default is pinned to `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5`; Studio 0.2.7's `cb34aa5` and legacy `2a3961c` remain reproducible, with SHA-256 verification for all binaries. +- Built-in breaking-change detection and safe migration for `entry`, `checkMsgSig`, `outpointTxId`, artifact `bytecode`, and explicit scalar byte/int conversions; signedness, removed `.reverse()`, and bitwise typing changes require manual review. - Pinned Kascov-derived local transaction preflight engine. - OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and OpenAI-compatible providers. - AES-256-GCM encrypted AI key vault with a scrypt-derived key. @@ -299,6 +302,7 @@ AI is limited to candidate generation and review assistance. It cannot unlock wa - One-time mnemonic display and optional BIP39 passphrase support. - Direct TN10 and mainnet self-hosted wRPC endpoints with public-node discovery fallback. - Portable `.ssinvite` operation packages, sequential cross-device signing, and external covenant-package review. +- Canonical v1 covenant descriptors bind each new package to its CAIP-2 network, program hash, covenant ID, ABI, state layout, and authorization principals; legacy packages remain readable with a visible missing-descriptor warning. - Replaceable `CovenantStateSource` fallback across native covenant RPC, outpoint RPC, and P2SH address indexing, with independent outpoint, covenant ID, script, and value verification. - Generic isolated P2PK co-spend authorization plus an atomic builder for 2–32 distinct covenant inputs. - Kascov is the preferred visual and secondary-report layer, not a signing, preflight, or broadcast dependency. @@ -314,6 +318,7 @@ AI is limited to candidate generation and review assistance. It cannot unlock wa | Multi-inheritor check-in vault | Owner check-in, owner recovery, mature distribution | Inheritance planning with periodic renewal | | Merkle one-time claim (TN10 Experimental) | Merkle proof claim, timeout refund | Allowlists and single-use tickets | | Commit / reveal (TN10 Experimental) | Reveal claim, timeout refund | Domain-separated commitments and sealed delivery | +| Groth16 proof release (TN10 Experimental) | ZK proof claim | Verifiable-computation payment to a fixed recipient | | Four-contract KCC721 pack (TN10 Experimental) | Collection, Ticket, NFT, Migration | Covenant-native NFT research; standalone deployment is blocked | Every template includes bilingual parameter forms and examples, deterministic constructor encoding, full compile verification, per-entrypoint transaction plans, and matching post-deployment builders. @@ -451,6 +456,7 @@ Never have multiple signers sign separate initial copies. Compare the transactio A covenant ID or cov hash alone is not a signing request. An external package must include the exact transaction, UTXOs, redeem program, ABI, entrypoint, arguments, outputs, and signature slots. See [Portable covenant packages](docs/portable-covenant-package.md). See [Studio 0.2 architecture](docs/studio-0.2-architecture.md) for compiler upgrades, state sources, P2PK authorization, and atomic transaction APIs. See [TN10 Experimental KCC721](docs/kcc721-experimental.md) for provenance, boundaries, and prohibited release claims. +See the [TN10 Experimental x402 profile](docs/x402-experimental-profile.md) for network identifiers, operation-package mapping, and executable-template admission gates. ### Networks diff --git a/config/compiler-profiles.json b/config/compiler-profiles.json index 929b4a0..c13eda3 100644 --- a/config/compiler-profiles.json +++ b/config/compiler-profiles.json @@ -1,16 +1,16 @@ { - "defaultProfileId": "latest-cb34aa5", + "defaultProfileId": "latest-6f9e078", "profiles": [ { - "id": "latest-cb34aa5", - "label": "SilverScript latest (cb34aa5)", - "upstreamCommit": "cb34aa5e6a598f9e461c4ad7014279ba89251d8d", + "id": "latest-6f9e078", + "label": "SilverScript latest (6f9e078)", + "upstreamCommit": "6f9e078b1d8b5389212755183b592704de99fea5", "binary": "bin/silverc-latest", "artifactBytecodeField": "bytecode", "syntaxGeneration": 2, "status": "experimental", "networkPolicy": "tn10-only", - "releasedAt": "2026-08-09", + "releasedAt": "2026-08-10", "notes": [ "Uses entry syntax for public entrypoints", "Exposes transaction introspection as outpointTxId", @@ -18,7 +18,24 @@ "Compiler JSON calls the emitted program bytecode", "Rejects duplicate function names and entry parameters that shadow contract fields", "Restricts ordered comparisons to numeric operands", - "Adds the variable-input g16.verify Groth16 verifier builtin" + "Adds the variable-input g16.verify Groth16 verifier builtin", + "Requires explicit signed(byte) or unsigned(byte) scalar conversion", + "Uses value as byte for checked runtime integer-to-byte conversion" + ] + }, + { + "id": "latest-cb34aa5", + "label": "SilverScript previous (cb34aa5)", + "upstreamCommit": "cb34aa5e6a598f9e461c4ad7014279ba89251d8d", + "binary": "bin/silverc-cb34aa5", + "artifactBytecodeField": "bytecode", + "syntaxGeneration": 2, + "status": "previous", + "networkPolicy": "tn10-only", + "releasedAt": "2026-08-09", + "notes": [ + "Retained to reproduce Studio 0.2.7 projects", + "Allows the pre-6f9e078 scalar byte conversion behavior" ] }, { @@ -43,7 +60,7 @@ "severity": "error", "introducedBy": "0f99803", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-cb34aa5", + "toProfile": "latest-6f9e078", "pattern": "\\bentrypoint\\s+function\\b", "messageZh": "公开入口语法已从 entrypoint function 改为 entry。", "messageEn": "Public entrypoint syntax changed from entrypoint function to entry.", @@ -54,7 +71,7 @@ "severity": "error", "introducedBy": "782a4d7", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-cb34aa5", + "toProfile": "latest-6f9e078", "pattern": "\\bcheckSigFromStack\\b", "messageZh": "任意消息签名内建函数已改名为 checkMsgSig。", "messageEn": "The arbitrary-message signature builtin was renamed to checkMsgSig.", @@ -65,7 +82,7 @@ "severity": "error", "introducedBy": "65421cf", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-cb34aa5", + "toProfile": "latest-6f9e078", "pattern": "\\.outpointTransactionHash\\b", "messageZh": "输入 outpoint 字段已改名为 outpointTxId。", "messageEn": "The input outpoint field was renamed to outpointTxId.", @@ -76,7 +93,7 @@ "severity": "error", "introducedBy": "6869e7d", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-cb34aa5", + "toProfile": "latest-6f9e078", "pattern": "\\.reverse\\s*\\(", "messageZh": ".reverse() 已删除,必须显式重写字节顺序逻辑并重新测试。", "messageEn": ".reverse() was removed; rewrite byte ordering explicitly and retest it.", @@ -87,7 +104,7 @@ "severity": "integration", "introducedBy": "4d88ded", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-cb34aa5", + "toProfile": "latest-6f9e078", "pattern": null, "messageZh": "编译产物 JSON 字段由 script 改为 bytecode;外部工具必须兼容两个字段。", "messageEn": "The compiler artifact JSON field changed from script to bytecode; integrations must support both.", @@ -98,7 +115,7 @@ "severity": "manual-review", "introducedBy": "8b74812", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-cb34aa5", + "toProfile": "latest-6f9e078", "pattern": "(?:^|[^&|])(?:<<|>>|\\^|&|\\|)(?:[^&|]|$)", "messageZh": "位运算现在仅接受 bytes;请人工确认操作数类型。", "messageEn": "Bitwise operators now accept bytes only; review operand types manually.", diff --git a/docs/kcc721-experimental.md b/docs/kcc721-experimental.md index 6771ee8..7b65a3b 100644 --- a/docs/kcc721-experimental.md +++ b/docs/kcc721-experimental.md @@ -6,7 +6,7 @@ Studio 内置的 KCC721 包改编自 `KaspaHUB21/KCC721` v0.2 社区草案,保 本包只允许 `tn10`,风险等级为 `high-experimental`。普通单 Covenant 部署路径被明确禁用,因为 Collection/Ticket/NFT 创世需要专用的多合约 builder 正确计算模板片段、模板哈希、Covenant ID 和 output binding。当前完成的是: -- 四份源码在官方 `silverc@cb34aa5` 下完整编译。 +- 四份源码在官方 `silverc@6f9e078` 下完整编译。 - 模板使用三步配置向导,不再要求用户手填元数据摘要。名称、描述、图片 URI、外部链接和属性会先规范化为确定性 JSON,再由前后端分别计算并核对 SHA-256。 - “新集合”明确标记为编译预览,内部使用不可部署的全零哨兵;只有“导入已有 TN10 集合”模式接受从真实创世输出核验的 Collection Covenant ID。 - NFT 所有者变更绑定独立 P2PK co-spend 输入。 @@ -34,7 +34,7 @@ The bundled KCC721 pack is adapted from the community `KaspaHUB21/KCC721` v0.2 d The pack is restricted to `tn10` and marked `high-experimental`. Ordinary single-covenant deployment is explicitly blocked because Collection/Ticket/NFT genesis requires a dedicated multi-contract builder to calculate template segments, template hashes, covenant IDs, and output bindings correctly. The current implementation provides: -- Full compilation of all four sources with official `silverc@cb34aa5`. +- Full compilation of all four sources with official `silverc@6f9e078`. - A three-step setup wizard that no longer asks users to type a metadata digest. Name, description, image URI, external URL, and attributes are canonicalized into deterministic JSON, then SHA-256 is independently recomputed by the client and server. - A clearly labeled new-collection compile preview with a non-deployable internal all-zero sentinel. Only the existing-TN10-collection path accepts a Collection covenant ID verified from a real genesis output. - NFT ownership transitions bound to a separate P2PK co-spend input. diff --git a/docs/portable-covenant-package.md b/docs/portable-covenant-package.md index bf6a2dc..b951d14 100644 --- a/docs/portable-covenant-package.md +++ b/docs/portable-covenant-package.md @@ -10,6 +10,7 @@ version-1 交易包。 { "version": 1, "network": "tn10", + "networkCaip2": "kaspa:testnet-10", "transactionSafeJson": "{...Kaspa Transaction Safe JSON...}", "covenantInput": { "index": 0, @@ -28,7 +29,20 @@ version-1 交易包。 "kind": "signature", "publicKey": "32-byte x-only public key" } - ] + ], + "descriptor": { + "schema": "kaspa-covenant-descriptor", + "version": 1, + "profileId": "producer/template/v1", + "network": "kaspa:testnet-10", + "programSha256": "32-byte hex", + "covenantId": "32-byte hex", + "abi": { "encoding": "silverscript-json-abi/v1", "sha256": "32-byte hex" }, + "state": { "encoding": "silverscript-state-layout/v1", "sha256": "32-byte hex" }, + "controlPrincipals": [], + "authorizationPrincipals": [] + }, + "descriptorSha256": "canonical descriptor SHA-256" }, "provenance": { "kind": "producer-defined", @@ -46,6 +60,15 @@ the complete transaction. 当前支持 `sig`、`pubkey`、`int`、`bool`、`byte[]` 和 `byte[N]` 参数。`sig` 参数代表签名槽;多个本地客户端可以依次导入同一个包并填充自己的签名槽。 +New Studio packages include a canonical version-1 descriptor. It binds the CAIP-2 +network, program, covenant ID, ABI, state layout, and recognized principal profiles. +Unknown principal profiles fail closed. Older packages without a descriptor remain +readable but are visibly marked as legacy and require stronger independent review. + +Studio 新生成的包包含 canonical v1 描述符,绑定 CAIP-2 网络、程序、Covenant ID、ABI、 +状态布局和已识别的主体类型。未知主体 profile 会失效关闭。没有描述符的旧包仍可读取, +但会明确标成旧版,并要求更严格的独立核对。 + ## Why covenant ID alone is insufficient / 为什么只有 covenant ID 不够 A covenant ID identifies a covenant domain, but it does not describe the transaction diff --git a/docs/releases/v0.2.8.md b/docs/releases/v0.2.8.md new file mode 100644 index 0000000..c64ac2a --- /dev/null +++ b/docs/releases/v0.2.8.md @@ -0,0 +1,47 @@ +## Kaspa SilverScript Studio v0.2.8 + +This release tracks official `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5` and hardens portable covenant authorization metadata. + +### Compiler compatibility + +- The default compiler is now `6f9e078`, with SHA-256 verification. +- `cb34aa5` remains available for Studio 0.2.7 projects and `2a3961c` for older reproducible builds. +- Compatibility review detects runtime `byte(...)` casts and ambiguous scalar `int(byte)` conversions. Studio never guesses signedness. + +### Versioned operation packages + +- New lifecycle and atomic `.ssinvite` files include a canonical v1 covenant descriptor. +- The descriptor commits to CAIP-2 network, program SHA-256, covenant ID, ABI, state layout, control-principal declarations, and operation authorization principals. +- Import recomputes those commitments and rejects tampering. Legacy descriptor-free packages remain readable with a visible warning. + +### TN10 experiments + +- A new Groth16 proof-release template uses official `g16.verify`, exact fixed-recipient payout, explicit bounded fees, and local engine preflight. +- Kaspa x402 is documented as a pinned alpha interoperability profile. Its current upstream escrow is not exposed as deployable until it is ported to current syntax, vectors are reproduced, and adversarial lifecycle tests pass. + +SilverScript and the new template remain experimental. Mainnet stays fail-closed; compilation and preflight are not independent security review. + +--- + +## 中文说明 + +本版跟进官方 `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5`,并强化可携带 Covenant 操作包的授权元数据。 + +### 编译器兼容 + +- 默认编译器升级到 `6f9e078`,继续校验二进制 SHA-256。 +- 保留 `cb34aa5` 复现 Studio 0.2.7 项目,并保留 `2a3961c` 复现更早项目。 +- 兼容性检查会发现运行时 `byte(...)` 和可能把标量 byte 传给 `int(...)` 的写法;Studio 不会自动猜测 signed/unsigned 语义。 + +### 版本化操作包 + +- 新生命周期和原子 `.ssinvite` 文件包含 canonical v1 Covenant Descriptor。 +- 描述符绑定 CAIP-2 网络、程序 SHA-256、Covenant ID、ABI、状态布局、控制主体声明和本次操作授权主体。 +- 导入时重新计算承诺并拒绝篡改。没有描述符的旧包仍可读取,但会醒目警告。 + +### TN10 实验功能 + +- 新增使用官方 `g16.verify` 的 Groth16 证明释放模板,固定收款钱包、精确守恒资金、限制显式手续费并执行本地引擎预检。 +- Kaspa x402 以固定 alpha 提交的互操作档案加入。上游托管合约在完成新语法迁移、向量复现和对抗性生命周期测试前不会显示成可直接部署模板。 + +SilverScript 和新模板仍属实验功能。主网继续失效关闭;成功编译和预检不能替代独立安全审查。 diff --git a/docs/studio-0.2-architecture.md b/docs/studio-0.2-architecture.md index ce0508b..6c8cc93 100644 --- a/docs/studio-0.2-architecture.md +++ b/docs/studio-0.2-architecture.md @@ -4,9 +4,9 @@ ### 编译器兼容档案 -`config/compiler-profiles.json` 是可提交的兼容性清单,`config/compiler.json` 是本机生成的二进制路径、构建时间和 SHA-256 清单。默认档案固定官方 SilverScript `cb34aa5e6a598f9e461c4ad7014279ba89251d8d`,旧版 `2a3961cadc76bb16a425042172ffe32481da89b5` 只用于复现已有项目。 +`config/compiler-profiles.json` 是可提交的兼容性清单,`config/compiler.json` 是本机生成的二进制路径、构建时间和 SHA-256 清单。默认档案固定官方 SilverScript `6f9e078b1d8b5389212755183b592704de99fea5`;`cb34aa5` 保留用于复现 Studio 0.2.7 项目,`2a3961c` 保留用于更早的旧版项目。 -升级检查会报告已知变化,并只自动替换无歧义的名称。`.reverse()` 删除、字节序、位运算类型和任何状态布局变化必须人工审查。迁移后仍必须使用真实构造参数完整编译并进行对抗性交易测试。 +升级检查会报告已知变化,并只自动替换无歧义的名称。`6f9e078` 要求标量 byte 转 int 时明确使用 `signed()` 或 `unsigned()`,运行时 int 转 byte 使用 `as byte`;这些语义不能自动猜测。`.reverse()` 删除、字节序、位运算类型和任何状态布局变化也必须人工审查。迁移后仍必须使用真实构造参数完整编译并进行对抗性交易测试。 接口: @@ -22,6 +22,12 @@ Studio 当前依次尝试:节点原生 Covenant ID 查询(节点支持时) 接口:`POST /api/covenants/resolve`。 +### 版本化 Covenant Descriptor + +Studio 新生成的 `.ssinvite` 为每个 Covenant 输入附带 canonical v1 descriptor。描述符固定 CAIP-2 网络、程序 SHA-256、Covenant ID、ABI 哈希、状态布局哈希、控制主体声明和本次授权主体,并单独提交 descriptor SHA-256。 + +导入时会从交易 UTXO 和 redeem program 重新计算这些值。ABI、状态布局、网络、程序或 Covenant ID 任一不匹配都会拒绝操作包。旧版操作包仍可读取,但界面会明确显示 `legacy-missing`;描述符只提高元数据完整性,不能证明 redeem program 的业务语义。 + ### P2PK co-spend 授权 普通 P2PK 输入可作为 Covenant 状态转换的独立钱包授权。选择器采用“覆盖所需金额的最小已确认非 Coinbase UTXO”。授权组件验证网络、地址与 x-only 公钥、P2PK script、outpoint 和金额。 @@ -48,9 +54,9 @@ Studio 当前依次尝试:节点原生 Covenant ID 查询(节点支持时) ### Compiler compatibility profiles -`config/compiler-profiles.json` is the committed compatibility catalog. The generated `config/compiler.json` records local binary paths, build times, and SHA-256 hashes. The default profile pins official SilverScript commit `cb34aa5e6a598f9e461c4ad7014279ba89251d8d`; `2a3961cadc76bb16a425042172ffe32481da89b5` is retained only for reproducible legacy builds. +`config/compiler-profiles.json` is the committed compatibility catalog. The generated `config/compiler.json` records local binary paths, build times, and SHA-256 hashes. The default profile pins official SilverScript commit `6f9e078b1d8b5389212755183b592704de99fea5`; `cb34aa5` reproduces Studio 0.2.7 projects and `2a3961c` remains for older legacy projects. -Compatibility checks report known changes and automatically apply only unambiguous renames. Removed `.reverse()`, byte ordering, bitwise typing, and any state-layout change require manual review. Every migration still requires a full compile with realistic constructor arguments and adversarial transaction tests. +Compatibility checks report known changes and automatically apply only unambiguous renames. Commit `6f9e078` requires explicit `signed()`/`unsigned()` conversion from scalar byte and `as byte` for checked runtime int conversion; Studio never guesses that meaning. Removed `.reverse()`, byte ordering, bitwise typing, and state-layout changes also require manual review. Every migration still requires a full compile with realistic constructor arguments and adversarial transaction tests. Endpoints: @@ -66,6 +72,12 @@ Studio currently tries native covenant-ID RPC when available, outpoint RPC when Endpoint: `POST /api/covenants/resolve`. +### Versioned covenant descriptor + +Every new `.ssinvite` includes a canonical v1 descriptor for each covenant input. It commits to the CAIP-2 network, program SHA-256, covenant ID, ABI hash, state-layout hash, declared control principals, and operation authorization principals, with a separate descriptor SHA-256. + +Import reconstructs these commitments from the transaction UTXO and redeem program. A mismatch in network, program, covenant ID, ABI, or state layout fails closed. Legacy packages remain readable but are visibly marked `legacy-missing`; a descriptor improves metadata integrity but does not prove redeem-program business semantics. + ### P2PK co-spend authorization A plain P2PK input can independently authorize a covenant state transition. The selector chooses the smallest confirmed, non-coinbase UTXO that covers the required value. Authorization verifies network, address/x-only key ownership, P2PK script, outpoint, and amount. diff --git a/docs/x402-experimental-profile.md b/docs/x402-experimental-profile.md new file mode 100644 index 0000000..6fad293 --- /dev/null +++ b/docs/x402-experimental-profile.md @@ -0,0 +1,48 @@ +# Kaspa x402 TN10 experimental profile / Kaspa x402 TN10 实验档案 + +Studio tracks the public Kaspa x402 alpha at source commit +`7cae0eeea174f4ed077c96f376af69a38d18eddb`. This is an interoperability +profile, not a mainnet-readiness claim and not an authorization source. + +Studio 跟踪公开的 Kaspa x402 alpha 源码提交 +`7cae0eeea174f4ed077c96f376af69a38d18eddb`。这是互操作档案,不代表主网成熟, +也不能作为资金授权来源。 + +## Network identifiers / 网络标识 + +- Studio internal: `tn10` +- Kaspa native: `testnet-10` +- proposed CAIP-2: `kaspa:testnet-10` + +New Studio operation packages carry both the internal network and the CAIP-2 +identifier. The CAIP registration remains a proposal, so import accepts the +aliases but canonical signing and transaction review still use the selected +Kaspa node network. + +新操作包同时携带 Studio 内部网络和 CAIP-2 标识。CAIP 注册仍是提案,因此导入可以识别 +这些别名,但签名和交易审查仍以实际选择的 Kaspa 节点网络为准。 + +## Safe Studio mapping / Studio 安全映射 + +| x402 concept | Studio representation | +|---|---| +| payment requirements | read-only request metadata; never wallet authority | +| exact payment | ordinary reviewed wallet-transfer draft | +| batch escrow deposit | compiled covenant deployment with exact artifact evidence | +| voucher | domain-separated off-chain message signature bound to network, script and active outpoint | +| claim/refund | versioned `.ssinvite` operation package and local engine preflight | +| settlement evidence | node txid plus optional Kascov visualization | + +Every future executable x402 template must pin the upstream source and compiler, +port the source to current `entry`, `checkMsgSig`, `outpointTxId`, and explicit +scalar byte conversions, reproduce the published vectors, and include claim, +continuation, refund, replay, wrong-network, wrong-outpoint, value-conservation, +and fee-boundary tests. Until those gates pass, Studio does not present the +upstream alpha escrow as a deployable built-in template. + +未来任何可执行 x402 模板都必须固定上游源码和编译器,迁移到当前 `entry`、 +`checkMsgSig`、`outpointTxId` 与显式标量 byte 转换,复现公开测试向量,并覆盖领取、 +延续、退款、重放、错误网络、错误 outpoint、价值守恒和手续费边界测试。在这些门槛完成前, +Studio 不会把上游 alpha 托管合约伪装成可直接部署的成熟内置模板。 + +Primary source: diff --git a/knowledge/kaspa-silverscript/references/official-baseline.md b/knowledge/kaspa-silverscript/references/official-baseline.md index 35b9902..410bbba 100644 --- a/knowledge/kaspa-silverscript/references/official-baseline.md +++ b/knowledge/kaspa-silverscript/references/official-baseline.md @@ -3,8 +3,8 @@ ## Verified snapshot - Repository: -- Verified commit: `cb34aa5e6a598f9e461c4ad7014279ba89251d8d` -- Verified date: 2026-08-09 +- Verified commit: `6f9e078b1d8b5389212755183b592704de99fea5` +- Verified date: 2026-08-10 - Compiler/language status: experimental - Official recommendation at this snapshot: use bytecode artifacts on testnet-10 until the first stable v1 release. @@ -15,17 +15,17 @@ deployment, compatibility, or mainnet-readiness questions. ## Primary sources - Project status and debugger: - + - Language tutorial: - + - Covenant declaration semantics: - + - Built-ins and cross-template validation: - + - KCC20 book: - Official application examples, including chess: - + ## Snapshot capabilities @@ -62,6 +62,11 @@ deployment, compatibility, or mainnet-readiness questions. shadow contract fields, and non-numeric ordered comparisons. It also fixes fixed/dynamic array sizing and cast validation. Commit `5aa0886` adds the variable-input `g16.verify` Groth16 verifier built-in. +- Commit `6f9e078` makes scalar byte/integer conversions explicit. `byte(...)` + accepts an existing scalar byte or an integer literal in `0..=255`; a runtime + integer uses checked `value as byte`. Scalar byte to integer conversion must + choose `signed(byteValue)` or `unsigned(byteValue)`, and scalar bytes cannot + participate directly in arithmetic operators. ## Terminology discipline diff --git a/knowledge/kaspa-silverscript/references/upstream.json b/knowledge/kaspa-silverscript/references/upstream.json index 59fd3eb..b5ae3fe 100644 --- a/knowledge/kaspa-silverscript/references/upstream.json +++ b/knowledge/kaspa-silverscript/references/upstream.json @@ -1,8 +1,8 @@ { "repository": "https://github.com/kaspanet/silverscript.git", "branch": "master", - "verified_commit": "cb34aa5e6a598f9e461c4ad7014279ba89251d8d", - "verified_at": "2026-08-09", + "verified_commit": "6f9e078b1d8b5389212755183b592704de99fea5", + "verified_at": "2026-08-10", "status": "experimental", "recommended_network": "testnet-10" } diff --git a/package-lock.json b/package-lock.json index b47c888..e4626fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.7", + "version": "0.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kaspa-silverscript-studio", - "version": "0.2.7", + "version": "0.2.8", "license": "MIT", "dependencies": { "@kluster/kaspa-wasm": "2.0.1", diff --git a/package.json b/package.json index f5777d5..6b5b8fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.7", + "version": "0.2.8", "private": true, "type": "module", "description": "Local bilingual AI-assisted SilverScript contract studio for Kaspa", diff --git a/scripts/build-silverc.mjs b/scripts/build-silverc.mjs index 3ce0a20..8d46afb 100644 --- a/scripts/build-silverc.mjs +++ b/scripts/build-silverc.mjs @@ -8,7 +8,8 @@ import { fileURLToPath } from "node:url"; import { cargoReleaseBinary, executableName, makeExecutable } from "./platform-binaries.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const latestCommit = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; +const latestCommit = "6f9e078b1d8b5389212755183b592704de99fea5"; +const previousCommit = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; const legacyCommit = "2a3961cadc76bb16a425042172ffe32481da89b5"; const work = fs.mkdtempSync(path.join(os.tmpdir(), "silverstudio-silverc-")); @@ -55,11 +56,17 @@ function buildProfile({ id, commit, outputName, configuredSource }) { try { fs.mkdirSync(path.join(root, "config"), { recursive: true }); const latestBin = buildProfile({ - id: "latest-cb34aa5", + id: "latest-6f9e078", commit: latestCommit, outputName: "silverc-latest", configuredSource: process.env.SILVERSCRIPT_LATEST_SOURCE || process.env.SILVERSCRIPT_SOURCE || "" }); + const previousBin = buildProfile({ + id: "latest-cb34aa5", + commit: previousCommit, + outputName: "silverc-cb34aa5", + configuredSource: process.env.SILVERSCRIPT_PREVIOUS_SOURCE || "" + }); const legacyBin = buildProfile({ id: "legacy-2a3961c", commit: legacyCommit, @@ -67,16 +74,23 @@ try { configuredSource: process.env.SILVERSCRIPT_LEGACY_SOURCE || "" }); const latestSha256 = sha256(latestBin); + const previousSha256 = sha256(previousBin); const legacySha256 = sha256(legacyBin); const manifest = { - defaultProfileId: "latest-cb34aa5", + defaultProfileId: "latest-6f9e078", profiles: { - "latest-cb34aa5": { + "latest-6f9e078": { bin: latestBin, sha256: latestSha256, upstreamCommit: latestCommit, builtAt: new Date().toISOString() }, + "latest-cb34aa5": { + bin: previousBin, + sha256: previousSha256, + upstreamCommit: previousCommit, + builtAt: new Date().toISOString() + }, "legacy-2a3961c": { bin: legacyBin, sha256: legacySha256, @@ -87,9 +101,12 @@ try { }; fs.writeFileSync(path.join(root, "config", "compiler.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 }); run(latestBin, ["--help"]); + run(previousBin, ["--help"]); run(legacyBin, ["--help"]); console.log(`latest silverc commit: ${latestCommit}`); console.log(`latest silverc sha256: ${latestSha256}`); + console.log(`previous silverc commit: ${previousCommit}`); + console.log(`previous silverc sha256: ${previousSha256}`); console.log(`legacy silverc commit: ${legacyCommit}`); console.log(`legacy silverc sha256: ${legacySha256}`); console.log(`manifest: ${path.join(root, "config", "compiler.json")}`); diff --git a/scripts/prepare-desktop-runtime.mjs b/scripts/prepare-desktop-runtime.mjs index f6884b5..2c185df 100644 --- a/scripts/prepare-desktop-runtime.mjs +++ b/scripts/prepare-desktop-runtime.mjs @@ -16,7 +16,7 @@ function copy(relative) { fs.cpSync(path.join(root, relative), destination, { recursive: true, force: true }); } -const helperNames = ["silverc-latest", "silverc-legacy", "kascov-preflight"]; +const helperNames = ["silverc-latest", "silverc-cb34aa5", "silverc-legacy", "kascov-preflight"]; for (const helper of helperNames) { const source = path.join(root, binaryRelativePath(helper)); if (!fs.existsSync(source)) throw new Error(`Pinned ${executableName(helper)} is missing. Run the matching setup command first.`); diff --git a/server/atomic-covenant-builder.mjs b/server/atomic-covenant-builder.mjs index af33a69..8ac0a36 100644 --- a/server/atomic-covenant-builder.mjs +++ b/server/atomic-covenant-builder.mjs @@ -1,6 +1,7 @@ import { createRequire } from "node:module"; import { NETWORKS } from "./config.mjs"; import { sha256 } from "./security.mjs"; +import { buildCovenantDescriptor, caip2Network } from "./covenant-descriptor.mjs"; const require = createRequire(import.meta.url); const kaspa = require("@kluster/kaspa-wasm"); @@ -89,15 +90,36 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena const programHex = cleanHex(item.programHex); const script = utxo?.scriptPublicKey?.script || utxo?.entry?.scriptPublicKey?.script; if (String(script || "").toLowerCase() !== kaspa.payToScriptHashScript(programHex).script) throw builderError("Atomic covenant redeem program does not match its input UTXO"); + const programSha256 = sha256(Buffer.from(programHex, "hex")); + const abi = item.abi; + const stateFields = item.stateFields || []; + const authorizationPrincipals = (item.arguments || []).map((argument, argumentIndex) => argument?.kind === "signature" ? { + role: `${item.entrypoint}.signature-${argumentIndex}`, + profile: "p2pk-schnorr/v1", + cardinality: 1, + reference: { kind: "public-key", value: argument.publicKey } + } : null).filter(Boolean); + const descriptor = buildCovenantDescriptor({ + profileId: item.descriptorProfileId || `silverstudio/atomic-input-${index}/v1`, + network: networkId, + programSha256, + covenantId, + abi, + stateFields, + controlPrincipals: item.controlPrincipals || [], + authorizationPrincipals + }); metadata.push({ index, covenantId, programHex, - programSha256: sha256(Buffer.from(programHex, "hex")), - abi: item.abi, - stateFields: item.stateFields || [], + programSha256, + abi, + stateFields, entrypoint: item.entrypoint, - arguments: item.arguments || [] + arguments: item.arguments || [], + descriptor: descriptor.descriptor, + descriptorSha256: descriptor.descriptorSha256 }); return { previousOutpoint: outpoint, @@ -147,6 +169,7 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena return { version: 1, network: network.id, + networkCaip2: caip2Network(network.id), transactionSafeJson: transaction.serializeToSafeJSON(), covenantInputs: metadata, ...(p2pkAuthorization?.metadata ? { p2pkAuthorization: { ...p2pkAuthorization.metadata, signed: false } } : {}), diff --git a/server/compiler.mjs b/server/compiler.mjs index 3fb602f..34fcb97 100644 --- a/server/compiler.mjs +++ b/server/compiler.mjs @@ -65,7 +65,7 @@ function lineAt(source, offset) { } function latestHardeningFindings(source, target) { - if (target.upstreamCommit !== "cb34aa5e6a598f9e461c4ad7014279ba89251d8d") return []; + if (!["cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "6f9e078b1d8b5389212755183b592704de99fea5"].includes(target.upstreamCommit)) return []; const findings = []; const declarations = new Map(); const functionPattern = /\b(?:entry|function)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g; @@ -135,6 +135,51 @@ function latestHardeningFindings(source, target) { return findings; } +function scalarConversionFindings(source, target) { + if (target.upstreamCommit !== "6f9e078b1d8b5389212755183b592704de99fea5") return []; + const findings = []; + const scalarByteCast = /\bbyte\s*\(\s*([^()\n]+?)\s*\)/g; + let match; + while ((match = scalarByteCast.exec(source))) { + const argument = match[1].trim(); + const decimal = argument.match(/^([0-9]+)$/); + const hex = argument.match(/^0x([0-9a-fA-F]+)$/); + const literal = decimal ? Number(decimal[1]) : hex ? Number.parseInt(hex[1], 16) : null; + if (Number.isSafeInteger(literal) && literal >= 0 && literal <= 255) continue; + findings.push({ + id: "explicit-runtime-int-to-byte", + severity: "manual-review", + introducedBy: "6f9e078", + fromProfile: "latest-cb34aa5", + toProfile: target.id, + pattern: null, + replacement: null, + line: lineAt(source, match.index), + detected: true, + messageZh: "byte(...) 现在只接受 byte 或 0..255 整数字面量;若参数是运行时 int,请人工改为 value as byte 并确认 128 等值会按脚本数编码在运行时失败。", + messageEn: "byte(...) now accepts only byte values or integer literals in 0..255. For a runtime int, review and use value as byte, noting that script-number encoding rejects values such as 128 at runtime." + }); + } + + const scalarIntCast = /\bint\s*\(\s*([A-Za-z_][A-Za-z0-9_.\[\]]*)\s*\)/g; + while ((match = scalarIntCast.exec(source))) { + findings.push({ + id: "explicit-byte-signedness", + severity: "manual-review", + introducedBy: "6f9e078", + fromProfile: "latest-cb34aa5", + toProfile: target.id, + pattern: null, + replacement: null, + line: lineAt(source, match.index), + detected: true, + messageZh: "若 int(...) 的参数是标量 byte,最新编译器要求明确选择 signed(byteValue) 或 unsigned(byteValue);必须根据协议含义人工判断。", + messageEn: "If this int(...) argument is a scalar byte, the latest compiler requires signed(byteValue) or unsigned(byteValue); choose manually from the protocol meaning." + }); + } + return findings; +} + function constructorParameterTypes(source) { const start = source.search(/\bcontract\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/); if (start < 0) return []; @@ -225,6 +270,7 @@ export function detectBreakingChanges(source, targetProfileId = config.compiler. } } findings.push(...latestHardeningFindings(text, target)); + findings.push(...scalarConversionFindings(text, target)); const blockers = findings.filter((finding) => finding.severity === "error"); return { targetProfileId: target.id, diff --git a/server/config.mjs b/server/config.mjs index cc063de..2233f12 100644 --- a/server/config.mjs +++ b/server/config.mjs @@ -8,7 +8,8 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); dotenv.config({ path: path.join(ROOT, ".env") }); dotenv.config({ path: path.join(ROOT, ".env.local"), override: true }); -export const SILVERSCRIPT_COMMIT = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; +export const SILVERSCRIPT_COMMIT = "6f9e078b1d8b5389212755183b592704de99fea5"; +export const SILVERSCRIPT_PREVIOUS_COMMIT = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; export const SILVERSCRIPT_LEGACY_COMMIT = "2a3961cadc76bb16a425042172ffe32481da89b5"; export const NETWORKS = Object.freeze({ @@ -54,8 +55,13 @@ function loadCompilerConfig() { const legacyStored = !stored.profiles && definition.upstreamCommit === (stored.upstreamCommit || SILVERSCRIPT_LEGACY_COMMIT) ? stored : {}; - const environmentBin = isLatest ? process.env.SILVERC_LATEST_BIN || process.env.SILVERC_BIN : process.env.SILVERC_LEGACY_BIN; - const environmentSha = isLatest ? process.env.SILVERC_LATEST_SHA256 || process.env.SILVERC_SHA256 : process.env.SILVERC_LEGACY_SHA256; + const isPrevious = definition.upstreamCommit === SILVERSCRIPT_PREVIOUS_COMMIT; + const environmentBin = isLatest + ? process.env.SILVERC_LATEST_BIN || process.env.SILVERC_BIN + : isPrevious ? process.env.SILVERC_PREVIOUS_BIN : process.env.SILVERC_LEGACY_BIN; + const environmentSha = isLatest + ? process.env.SILVERC_LATEST_SHA256 || process.env.SILVERC_SHA256 + : isPrevious ? process.env.SILVERC_PREVIOUS_SHA256 : process.env.SILVERC_LEGACY_SHA256; return [definition.id, Object.freeze({ ...definition, bin: path.resolve(environmentBin || local.bin || legacyStored.bin || path.join(ROOT, definition.binary)), @@ -65,7 +71,7 @@ function loadCompilerConfig() { })); const defaultProfileId = profiles[stored.defaultProfileId] ? stored.defaultProfileId - : compatibility.defaultProfileId || "latest-cb34aa5"; + : compatibility.defaultProfileId || "latest-6f9e078"; return Object.freeze({ defaultProfileId, profiles: Object.freeze(profiles), diff --git a/server/covenant-descriptor.mjs b/server/covenant-descriptor.mjs new file mode 100644 index 0000000..850c967 --- /dev/null +++ b/server/covenant-descriptor.mjs @@ -0,0 +1,140 @@ +import { sha256 } from "./security.mjs"; + +const NETWORK_ALIASES = Object.freeze({ + tn10: "tn10", + "testnet-10": "tn10", + "kaspa:testnet-10": "tn10", + mainnet: "mainnet", + "kaspa:mainnet": "mainnet" +}); + +const CAIP2 = Object.freeze({ tn10: "kaspa:testnet-10", mainnet: "kaspa:mainnet" }); +const PRINCIPAL_PROFILES = new Set(["p2pk-schnorr/v1", "covenant-id/v1", "program-hash/v1"]); + +function descriptorError(message, code = "INVALID_COVENANT_DESCRIPTOR") { + return Object.assign(new Error(message), { status: 400, code }); +} + +function canonicalValue(value) { + if (Array.isArray(value)) return value.map(canonicalValue); + if (!value || typeof value !== "object") return value; + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])])); +} + +export function canonicalJson(value) { + return JSON.stringify(canonicalValue(value)); +} + +export function canonicalSha256(value) { + return sha256(canonicalJson(value)); +} + +export function normalizePackageNetwork(value) { + return NETWORK_ALIASES[String(value || "").trim().toLowerCase()] || ""; +} + +export function caip2Network(network) { + return CAIP2[normalizePackageNetwork(network)] || ""; +} + +function cleanHex32(value, label) { + const normalized = String(value || "").trim().toLowerCase().replace(/^0x/, ""); + if (!/^[0-9a-f]{64}$/.test(normalized)) throw descriptorError(`${label} must contain exactly 32 bytes of hexadecimal data`); + return normalized; +} + +function cleanProfileId(value) { + const normalized = String(value || "").trim(); + if (!/^[a-z0-9][a-z0-9._/-]{2,127}$/i.test(normalized)) throw descriptorError("Covenant descriptor profileId is invalid"); + return normalized; +} + +function cleanPrincipal(principal, index) { + const role = String(principal?.role || "").trim(); + const profile = String(principal?.profile || "").trim().toLowerCase(); + if (!/^[a-z][a-z0-9._/-]{1,63}$/i.test(role)) throw descriptorError(`Covenant principal ${index} has an invalid role`); + if (!PRINCIPAL_PROFILES.has(profile)) throw descriptorError(`Covenant principal ${role} uses an unsupported profile`, "UNSUPPORTED_PRINCIPAL_PROFILE"); + const cardinality = Number(principal?.cardinality ?? 1); + if (!Number.isSafeInteger(cardinality) || cardinality < 1 || cardinality > 32) throw descriptorError(`Covenant principal ${role} has invalid cardinality`); + const reference = principal?.reference && typeof principal.reference === "object" ? principal.reference : null; + let normalizedReference = null; + if (reference) { + const kind = String(reference.kind || "").trim(); + const expectedKind = { + "p2pk-schnorr/v1": "public-key", + "covenant-id/v1": "covenant-id", + "program-hash/v1": "program-hash" + }[profile]; + if (kind !== expectedKind) throw descriptorError(`Covenant principal ${role} reference kind does not match profile ${profile}`); + const value = cleanHex32(reference.value, `Covenant principal ${role} reference`); + normalizedReference = { kind, value }; + } + return { + role, + profile, + cardinality, + ...(principal?.stateField ? { stateField: String(principal.stateField) } : {}), + ...(principal?.constructorParameter ? { constructorParameter: String(principal.constructorParameter) } : {}), + ...(normalizedReference ? { reference: normalizedReference } : {}) + }; +} + +export function buildCovenantDescriptor({ + profileId, + network, + programSha256, + covenantId, + abi, + stateFields = [], + controlPrincipals = [], + authorizationPrincipals = [] +}) { + const normalizedNetwork = caip2Network(network); + if (!normalizedNetwork) throw descriptorError("Covenant descriptor network is unsupported"); + const descriptor = { + schema: "kaspa-covenant-descriptor", + version: 1, + profileId: cleanProfileId(profileId), + network: normalizedNetwork, + programSha256: cleanHex32(programSha256, "Covenant descriptor programSha256"), + covenantId: cleanHex32(covenantId, "Covenant descriptor covenantId"), + abi: { + encoding: "silverscript-json-abi/v1", + sha256: canonicalSha256(abi) + }, + state: { + encoding: "silverscript-state-layout/v1", + sha256: canonicalSha256(stateFields) + }, + controlPrincipals: controlPrincipals.map(cleanPrincipal), + authorizationPrincipals: authorizationPrincipals.map(cleanPrincipal) + }; + return { descriptor, descriptorSha256: canonicalSha256(descriptor) }; +} + +export function verifyCovenantDescriptor(input, expected) { + if (!input || typeof input !== "object" || Array.isArray(input)) throw descriptorError("Covenant descriptor is missing"); + if (input.schema !== "kaspa-covenant-descriptor" || input.version !== 1) throw descriptorError("Covenant descriptor schema/version is unsupported"); + const built = buildCovenantDescriptor({ + profileId: input.profileId, + network: input.network, + programSha256: input.programSha256, + covenantId: input.covenantId, + abi: expected.abi, + stateFields: expected.stateFields, + controlPrincipals: Array.isArray(input.controlPrincipals) ? input.controlPrincipals : [], + authorizationPrincipals: Array.isArray(input.authorizationPrincipals) ? input.authorizationPrincipals : [] + }); + if (built.descriptor.network !== caip2Network(expected.network)) throw descriptorError("Covenant descriptor network does not match the package"); + if (built.descriptor.programSha256 !== expected.programSha256) throw descriptorError("Covenant descriptor program hash does not match the redeem program"); + if (built.descriptor.covenantId !== expected.covenantId) throw descriptorError("Covenant descriptor ID does not match the target UTXO"); + if (input.abi?.encoding !== built.descriptor.abi.encoding || String(input.abi?.sha256 || "").toLowerCase() !== built.descriptor.abi.sha256) { + throw descriptorError("Covenant descriptor ABI commitment does not match the supplied ABI"); + } + if (input.state?.encoding !== built.descriptor.state.encoding || String(input.state?.sha256 || "").toLowerCase() !== built.descriptor.state.sha256) { + throw descriptorError("Covenant descriptor state-layout commitment does not match the supplied state fields"); + } + const declaredHash = String(expected.descriptorSha256 || "").toLowerCase(); + if (declaredHash && declaredHash !== built.descriptorSha256) throw descriptorError("Covenant descriptor SHA-256 commitment is invalid"); + return built; +} diff --git a/server/external-covenant-service.mjs b/server/external-covenant-service.mjs index 27503a7..6f3e671 100644 --- a/server/external-covenant-service.mjs +++ b/server/external-covenant-service.mjs @@ -6,6 +6,7 @@ import { config, NETWORKS } from "./config.mjs"; import { sha256, transactionCommitment } from "./security.mjs"; import { kascovPreflight, sompiToKas } from "./kaspa-service.mjs"; import { operationPresentation } from "./operation-metadata.mjs"; +import { caip2Network, normalizePackageNetwork, verifyCovenantDescriptor } from "./covenant-descriptor.mjs"; const require = createRequire(import.meta.url); const kaspa = require("@kluster/kaspa-wasm"); @@ -23,7 +24,13 @@ function parsePackage(input) { let parsed; try { parsed = typeof input === "string" ? JSON.parse(input) : structuredClone(input || {}); } catch { throw packageError("External covenant package is not valid JSON"); } if (parsed.version !== 1) throw packageError("External covenant package version must be 1"); + const originalNetwork = parsed.network; + parsed.network = normalizePackageNetwork(parsed.network); if (!NETWORKS[parsed.network]) throw packageError("External covenant package network is unsupported"); + const expectedCaip2 = caip2Network(parsed.network); + if (parsed.networkCaip2 && parsed.networkCaip2 !== expectedCaip2) throw packageError("External covenant package CAIP-2 network does not match its network"); + parsed.networkCaip2 = expectedCaip2; + if (originalNetwork !== parsed.network) parsed.networkAlias = String(originalNetwork); if (typeof parsed.transactionSafeJson !== "string") parsed.transactionSafeJson = JSON.stringify(parsed.transactionSafeJson || {}); const metadata = Array.isArray(parsed.covenantInputs) ? parsed.covenantInputs @@ -106,12 +113,23 @@ function normalizedCovenant(transaction, metadata) { } } const stateFields = selected.inputs.some((input) => input.type_name === "State") ? cleanStateFields(metadata.stateFields) : []; - return { input, inputIndex, programHex, covenantId, abi, selected, argumentsList, stateFields }; + const programSha256 = sha256(Buffer.from(programHex, "hex")); + const descriptor = metadata.descriptor + ? verifyCovenantDescriptor(metadata.descriptor, { + network: metadata.network || "", + programSha256, + covenantId, + abi, + stateFields, + descriptorSha256: metadata.descriptorSha256 + }) + : null; + return { input, inputIndex, programHex, programSha256, covenantId, abi, selected, argumentsList, stateFields, descriptor }; } function normalized(pkg) { const transaction = transactionFrom(pkg); - const covenants = pkg.covenantInputs.map((metadata) => normalizedCovenant(transaction, metadata)); + const covenants = pkg.covenantInputs.map((metadata) => normalizedCovenant(transaction, { ...metadata, network: pkg.network })); if (new Set(covenants.map((item) => item.inputIndex)).size !== covenants.length) throw packageError("Covenant metadata records must target different transaction inputs"); let inputTotal = 0n; for (const item of transaction.inputs) { @@ -248,7 +266,11 @@ export function inspectExternalCovenantPackage(input) { covenantId: covenant.covenantId, abi: covenant.abi, arguments: covenant.argumentsList, - stateFields: covenant.stateFields + stateFields: covenant.stateFields, + ...(covenant.descriptor ? { + descriptor: covenant.descriptor.descriptor, + descriptorSha256: covenant.descriptor.descriptorSha256 + } : {}) })); const p2pkSigned = !p2pkAuthorization || p2pkAuthorization.signed; const operation = operationPresentation({ @@ -287,15 +309,21 @@ export function inspectExternalCovenantPackage(input) { covenantInputs: resolved.covenants.map((item) => ({ transactionInputIndex: item.inputIndex, covenantId: item.covenantId, - programSha256: sha256(Buffer.from(item.programHex, "hex")), + programSha256: item.programSha256, entrypoint: item.selected.name, - signatureCount: signatureSlots(item.selected, item.argumentsList).length + signatureCount: signatureSlots(item.selected, item.argumentsList).length, + descriptorStatus: item.descriptor ? "verified-v1" : "legacy-missing", + descriptorSha256: item.descriptor?.descriptorSha256 || "" })), + descriptorStatus: resolved.covenants.every((item) => item.descriptor) ? "verified-v1" : "legacy-missing", + descriptorSha256: resolved.descriptor?.descriptorSha256 || "", p2pkAuthorization, operation, atomic: resolved.covenants.length > 1, complete: slots.every((slot) => slot.signed) && p2pkSigned, - warning: "The supplied ABI is metadata, not proof of the redeem program semantics. Review trusted source/artifact provenance before signing." + warning: resolved.covenants.every((item) => item.descriptor) + ? "The versioned descriptor and ABI commitments match this package, but metadata still does not prove redeem-program semantics. Review trusted source/artifact provenance before signing." + : "Legacy package: no versioned descriptor is present. The supplied ABI is metadata, not proof of redeem-program semantics; review trusted source/artifact provenance before signing." } }; } diff --git a/server/project-store.mjs b/server/project-store.mjs index 8d2da72..35a7089 100644 --- a/server/project-store.mjs +++ b/server/project-store.mjs @@ -52,7 +52,7 @@ export class ProjectStore { requirements: String(input.requirements || ""), source: String(input.source || SAMPLE_SOURCE), constructorArgs: Array.isArray(input.constructorArgs) ? input.constructorArgs : [], - compilerProfileId: String(input.compilerProfileId || "latest-cb34aa5"), + compilerProfileId: String(input.compilerProfileId || "latest-6f9e078"), templateParameters: input.templateParameters && typeof input.templateParameters === "object" ? input.templateParameters : {}, deployAmount: String(input.deployAmount || "0.5"), specification: input.specification || null, diff --git a/server/template-operation-service.mjs b/server/template-operation-service.mjs index 039b077..abcf04b 100644 --- a/server/template-operation-service.mjs +++ b/server/template-operation-service.mjs @@ -3,6 +3,7 @@ import { createRequire } from "node:module"; import { NETWORKS } from "./config.mjs"; import { finalizeExternalCovenantPackage, inspectExternalCovenantPackage } from "./external-covenant-service.mjs"; import { findCovenantUtxo, kasToSompi, kascovPreflight, sompiToKas } from "./kaspa-service.mjs"; +import { buildCovenantDescriptor, caip2Network } from "./covenant-descriptor.mjs"; const require = createRequire(import.meta.url); const kaspa = require("@kluster/kaspa-wasm"); @@ -41,6 +42,9 @@ const OPERATIONS = { "commit-reveal": [ { id: "reveal", titleZh: "公开承诺原文领取", titleEn: "Reveal committed payload", payload: true, salt: true }, { id: "refund", titleZh: "到期退款", titleEn: "Timeout refund" } + ], + "groth16-proof-release": [ + { id: "claim", titleZh: "提交 Groth16 证明释放", titleEn: "Release with Groth16 proof", proof: true, proofKind: "groth16" } ] }; @@ -127,6 +131,18 @@ function timeoutOf(project, templateId) { return value; } +function controlPrincipalsOf(template, project) { + return (Array.isArray(template.controlPrincipals) ? template.controlPrincipals : []).map((principal) => { + const normalized = { ...principal }; + if (principal.cardinalityParameter) { + const value = project.templateParameters?.[principal.cardinalityParameter]; + normalized.cardinality = Array.isArray(value) ? value.length : 0; + delete normalized.cardinalityParameter; + } + return normalized; + }); +} + function inheritOutputs(parameters, value, network) { const inheritors = parameters?.inheritors; if (!Array.isArray(inheritors) || inheritors.length < 2 || inheritors.length > 5) throw operationError("Inheritance parameters are missing"); @@ -256,6 +272,11 @@ export async function buildTemplateOperationPackage( sigOps = 1; lockTime = BigInt(timeoutOf(project, templateId)); } + } else if (templateId === "groth16-proof-release") { + const identity = publicKeyOf(parameters.recipientAddress, network); + const proofHex = operationHex(input.proofHex, "Groth16 proof", { minimumBytes: 1, maximumBytes: 520 }); + outputs = [new kaspa.TransactionOutput(payout, kaspa.payToAddressScript(identity.address))]; + args = [bytesArgument(proofHex), int(fee)]; } else if (templateId === "commit-reveal") { const isReveal = operation.id === "reveal"; const identity = publicKeyOf(isReveal ? parameters.recipientAddress : parameters.senderAddress, network); @@ -293,9 +314,28 @@ export async function buildTemplateOperationPackage( gas: 0n, payload: "" }); + const authorizationPrincipals = args + .map((argument, index) => argument?.kind === "signature" ? { + role: `${operation.id}.signature-${index}`, + profile: "p2pk-schnorr/v1", + cardinality: 1, + reference: { kind: "public-key", value: argument.publicKey } + } : null) + .filter(Boolean); + const descriptor = buildCovenantDescriptor({ + profileId: template.descriptorProfileId || `silverstudio/${templateId}/v1`, + network: project.network, + programSha256: project.artifact.programSha256, + covenantId: source.covenantId, + abi: project.artifact.abi, + stateFields: project.artifact.stateFields || [], + controlPrincipals: controlPrincipalsOf(template, project), + authorizationPrincipals + }); const packageValue = { version: 1, network: project.network, + networkCaip2: caip2Network(project.network), transactionSafeJson: transaction.serializeToSafeJSON(), covenantInput: { index: 0, @@ -304,7 +344,9 @@ export async function buildTemplateOperationPackage( programSha256: project.artifact.programSha256, abi: project.artifact.abi, entrypoint: operation.id, - arguments: args + arguments: args, + descriptor: descriptor.descriptor, + descriptorSha256: descriptor.descriptorSha256 }, provenance: { kind: "silverstudio-template-operation", diff --git a/server/template-store.mjs b/server/template-store.mjs index d6da52f..083f674 100644 --- a/server/template-store.mjs +++ b/server/template-store.mjs @@ -83,6 +83,14 @@ function hex32(value) { return text; } +function hexBytes(value, minimumBytes = 1, maximumBytes = 520) { + const text = String(value || "").trim().toLowerCase().replace(/^0x/, ""); + if (!/^[0-9a-f]*$/.test(text) || text.length % 2) throw parameterError("Template byte data must be valid hexadecimal data"); + const length = text.length / 2; + if (length < minimumBytes || length > maximumBytes) throw parameterError(`Template byte data must contain ${minimumBytes}-${maximumBytes} bytes`); + return { hex: text, data: Array.from(Buffer.from(text, "hex")) }; +} + function durationDays(value, minimum = 1, maximum = 3650) { const days = Number(String(value ?? "").trim()); if (!Number.isSafeInteger(days) || days < minimum || days > maximum) { @@ -235,6 +243,10 @@ export class TemplateStore { const value = hex32(raw); parameters[field.id] = value; if (Number.isInteger(field.argIndex)) constructorArgs[field.argIndex] = compilerExpression({ kind: "bytes32", hex: value }); + } else if (field.type === "hexBytes") { + const value = hexBytes(raw, Number(field.minimumBytes || 1), Number(field.maximumBytes || 520)); + parameters[field.id] = value.hex; + if (Number.isInteger(field.argIndex)) constructorArgs[field.argIndex] = compilerExpression({ kind: "byte[]", data: value.data }); } else if (field.type === "choice") { const value = String(raw || "").trim(); const options = Array.isArray(field.options) ? field.options.map((option) => String(option.value)) : []; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8eee447..40231b7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -66,7 +66,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "app" -version = "0.2.7" +version = "0.2.8" dependencies = [ "log", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 08a5009..a688aee 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.2.7" +version = "0.2.8" description = "Local-first Kaspa SilverScript covenant workbench" authors = ["w00c00"] license = "MIT" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b732c45..cc00ee9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,7 +35,7 @@ fn append_backend_log(path: &Path, message: &str) { } } -fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf), String> { +fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf), String> { let resource_dir = app .path() .resource_dir() @@ -44,6 +44,9 @@ fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf) Ok(( resource_dir.join("runtime/app/server/index.mjs"), resource_dir.join(format!("runtime/app/bin/silverc-latest{executable_suffix}")), + resource_dir.join(format!( + "runtime/app/bin/silverc-cb34aa5{executable_suffix}" + )), resource_dir.join(format!("runtime/app/bin/silverc-legacy{executable_suffix}")), resource_dir.join(format!( "runtime/app/bin/kascov-preflight{executable_suffix}" @@ -75,10 +78,12 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { let log_path = app_data_dir.join("backend.log"); *app.state::().log_path.lock().unwrap() = Some(log_path.clone()); - let (script, latest_compiler, legacy_compiler, preflight_engine) = runtime_paths(app)?; + let (script, latest_compiler, previous_compiler, legacy_compiler, preflight_engine) = + runtime_paths(app)?; let required = [ &script, &latest_compiler, + &previous_compiler, &legacy_compiler, &preflight_engine, ]; @@ -108,6 +113,7 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { let script_arg = child_process_path(&script); let data_arg = child_process_path(&app_data_dir); let latest_compiler_arg = child_process_path(&latest_compiler); + let previous_compiler_arg = child_process_path(&previous_compiler); let legacy_compiler_arg = child_process_path(&legacy_compiler); let preflight_engine_arg = child_process_path(&preflight_engine); @@ -120,6 +126,7 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { .env("PORT", "4310") .env("STUDIO_DATA_DIR", data_arg) .env("SILVERC_LATEST_BIN", latest_compiler_arg) + .env("SILVERC_PREVIOUS_BIN", previous_compiler_arg) .env("SILVERC_LEGACY_BIN", legacy_compiler_arg) .env("KASCOV_PREFLIGHT_BIN", preflight_engine_arg) .spawn() diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9f9614d..40dfbcb 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.7", + "version": "0.2.8", "identifier": "io.kaspa.silverscript-studio", "build": { "frontendDist": "../dist", diff --git a/src/main.js b/src/main.js index 14947b8..bcc10f5 100644 --- a/src/main.js +++ b/src/main.js @@ -255,7 +255,7 @@ function projectPayload() { requirements: $("#requirements").value, source: $("#source-editor").value, constructorArgs, - compilerProfileId: $("#compiler-profile").value || state.config?.compiler?.defaultProfileId || "latest-cb34aa5", + compilerProfileId: $("#compiler-profile").value || state.config?.compiler?.defaultProfileId || "latest-6f9e078", templateParameters: state.project?.templateParameters || {}, deployAmount: $("#deploy-amount").value, specification: state.project?.specification || null, @@ -323,7 +323,7 @@ function loadProjectIntoUi(project) { $("#requirements").value = project.requirements || ""; $("#source-editor").value = project.source || ""; $("#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"; + $("#compiler-profile").value = project.compilerProfileId || project.artifact?.compiler?.id || project.review?.compilerProfileId || state.config?.compiler?.defaultProfileId || "latest-6f9e078"; renderCompilerProfileHelp(); $("#deploy-amount").value = Number(project.deployAmount || 0) >= 0.5 ? project.deployAmount : "0.5"; $("#deploy-network").value = project.network || "tn10"; @@ -454,6 +454,7 @@ function templateFieldInput(field, value) { if (field.type === "amount") return ``; if (field.type === "datetime") return ``; if (field.type === "sha256") return ``; + if (field.type === "hexBytes") return ``; if (field.type === "choice") return ``; if (field.type === "kcc721CollectionId") return ``; if (field.type === "integer") return ``; @@ -1493,6 +1494,17 @@ function renderLifecycleOperations() { renderLifecycleDestinationDefault(); $("#lifecycle-secret-row").hidden = !operation?.secret; $("#lifecycle-proof-row").hidden = !operation?.proof; + if (operation?.proof) { + const label = $("#lifecycle-proof-row span"); + const input = $("#lifecycle-proof"); + if (operation.proofKind === "groth16") { + label.textContent = state.language === "zh" ? "Groth16 证明(十六进制)" : "Groth16 proof (hex)"; + input.placeholder = state.language === "zh" ? "粘贴压缩 Groth16 证明" : "Paste the compressed Groth16 proof"; + } else { + label.textContent = tr("merkleProofHex"); + input.placeholder = state.language === "zh" ? "按顺序拼接 32-byte siblings;单叶树留空" : "Concatenate 32-byte siblings; leave empty for a single-leaf tree"; + } + } $("#lifecycle-payload-row").hidden = !operation?.payload; $("#lifecycle-salt-row").hidden = !operation?.salt; $("#lifecycle-signers-row").hidden = !operation?.signers; @@ -1754,6 +1766,7 @@ function renderExternalCovenantReview(review) { } const slots = review.signatureSlots || []; const p2pk = review.p2pkAuthorization || null; + const descriptorVerified = review.descriptorStatus === "verified-v1"; const outputs = (review.outputs || []).map((output) => `
  • #${output.index} · ${esc(output.valueKas)} ${review.network === "mainnet" ? "KAS" : "TKAS"} → ${esc(output.address ? short(output.address, 14, 10) : "non-address script")}${output.covenantId ? ` · cov ${esc(short(output.covenantId, 8, 7))}` : ""}
  • `).join(""); el.innerHTML = `
    ${state.language === "zh" ? "网络" : "Network"}${esc(review.network)}
    @@ -1765,7 +1778,12 @@ function renderExternalCovenantReview(review) {
    ${state.language === "zh" ? "签名槽" : "Signature slots"}${slots.filter((slot) => slot.signed).length}/${slots.length}
    ${p2pk ? `
    P2PK co-spend${p2pk.signed ? (state.language === "zh" ? "已签名" : "Signed") : (state.language === "zh" ? "等待拥有者" : "Awaiting owner")}
    ` : ""}
    ${state.language === "zh" ? "交易承诺" : "Commitment"}${esc(short(review.commitment, 12, 10))}
    -
      ${outputs}

    ${esc(state.language === "zh" ? "ABI 是外部元数据,不能证明 redeem program 的真实语义;签名前必须从可信来源核对源码和 artifact。" : review.warning)}

    `; +
    ${state.language === "zh" ? "描述符" : "Descriptor"}${descriptorVerified ? `v1 · ${esc(short(review.descriptorSha256, 10, 8))}` : (state.language === "zh" ? "旧包/缺失" : "Legacy / missing")}
    +
      ${outputs}

    ${esc(state.language === "zh" + ? (descriptorVerified + ? "版本化描述符、ABI 和状态布局承诺已匹配,但元数据仍不能证明 redeem program 的真实语义;签名前必须从可信来源核对源码和 artifact。" + : "旧版操作包没有版本化描述符。ABI 是外部元数据,不能证明 redeem program 的真实语义;签名前必须从可信来源核对源码和 artifact。") + : review.warning)}

    `; const hasUnsignedSlot = slots.some((slot) => !slot.signed); const hasUnsignedP2pk = Boolean(p2pk && !p2pk.signed); const canSignSlot = Boolean(state.wallet?.kind === "local" && slots.some((slot) => !slot.signed && slot.publicKey === state.wallet.publicKey)); diff --git a/templates/commit-reveal/manifest.json b/templates/commit-reveal/manifest.json index a2160f8..6fa0f66 100644 --- a/templates/commit-reveal/manifest.json +++ b/templates/commit-reveal/manifest.json @@ -7,10 +7,14 @@ "descriptionEn": "Deployment fixes a domain and commitment; the recipient reveals payload and salt to claim, or the sender refunds after timeout. TN10 experimental only.", "experimentalOnly": true, "networkAllowlist": ["tn10"], - "compilerProfileId": "latest-cb34aa5", + "compilerProfileId": "latest-6f9e078", "requirementsEn": "Release a whole escrow only when a domain-separated payload commitment is revealed, with an authenticated timeout refund.", "risk": "experimental", "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "sender", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "sender" }, + { "role": "recipient", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "recipient" } + ], "requiredReplacements": [0, 1, 2, 3, 4], "parameters": [ { "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." }, diff --git a/templates/groth16-proof-release/contract.sil b/templates/groth16-proof-release/contract.sil new file mode 100644 index 0000000..82cd001 --- /dev/null +++ b/templates/groth16-proof-release/contract.sil @@ -0,0 +1,35 @@ +pragma silverscript ^0.1.0; + +// TN10 research template. A valid Groth16 proof releases the complete covenant +// value, minus an explicit bounded fee, to one constructor-bound recipient. +contract Groth16ProofRelease( + byte[] verifyingKey, + byte[32] publicInput0, + byte[32] publicInput1, + byte[32] publicInput2, + byte[32] publicInput3, + byte[32] publicInput4, + pubkey recipient +) { + int constant MAX_PROOF_BYTES = 520; + int constant MAX_MINER_FEE = 10000000; + + entry claim(byte[] proof, int minerFee) { + require(verifyingKey.length > 0); + require(verifyingKey.length <= 520); + require(proof.length > 0); + require(proof.length <= MAX_PROOF_BYTES); + g16.verify(verifyingKey, proof, publicInput0, publicInput1, publicInput2, publicInput3, publicInput4); + + require(tx.inputs.length == 1); + require(this.activeInputIndex == 0); + require(tx.outputs.length == 1); + require(OpAuthOutputCount(this.activeInputIndex) == 0); + require(minerFee >= 1000); + require(minerFee <= MAX_MINER_FEE); + require(tx.inputs[this.activeInputIndex].value > minerFee); + byte[34] recipientScript = new ScriptPubKeyP2PK(recipient); + require(tx.outputs[0].scriptPubKey == byte[](recipientScript)); + require(tx.outputs[0].value == tx.inputs[this.activeInputIndex].value - minerFee); + } +} diff --git a/templates/groth16-proof-release/manifest.json b/templates/groth16-proof-release/manifest.json new file mode 100644 index 0000000..e21a8a5 --- /dev/null +++ b/templates/groth16-proof-release/manifest.json @@ -0,0 +1,61 @@ +{ + "order": 80, + "category": "zero-knowledge", + "titleZh": "Groth16 证明释放(TN10 Experimental)", + "titleEn": "Groth16 proof release (TN10 Experimental)", + "descriptionZh": "验证固定验证密钥和五个 32 字节公共输入对应的 Groth16 证明后,将全部资金减去显式手续费发送到固定收款钱包。", + "descriptionEn": "Verify a Groth16 proof against a fixed verifying key and five 32-byte public inputs, then pay the full value minus an explicit fee to a fixed recipient.", + "experimentalOnly": true, + "networkAllowlist": ["tn10"], + "compilerProfileId": "latest-6f9e078", + "descriptorProfileId": "silverstudio/groth16-proof-release/v1", + "requirementsEn": "Release funds only after the pinned Kaspa script engine accepts a Groth16 proof for the exact five configured public inputs.", + "risk": "experimental", + "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "recipient", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "recipient" } + ], + "requiredReplacements": [0, 1, 2, 3, 4, 5, 6], + "parameters": [ + { "id": "amountKas", "type": "amount", "projectField": "deployAmount", "required": true, "default": "0.5", "minimum": "0.5", "labelZh": "锁定金额", "labelEn": "Locked amount", "helpZh": "仅限 TN10;证明成功后全额减去显式手续费支付。", "helpEn": "TN10 only; a valid proof pays the complete value minus the explicit fee." }, + { "id": "verifyingKey", "type": "hexBytes", "argIndex": 0, "required": true, "minimumBytes": 1, "maximumBytes": 520, "labelZh": "Groth16 压缩验证密钥(十六进制)", "labelEn": "Compressed Groth16 verifying key (hex)", "placeholderZh": "粘贴 1–520 字节十六进制验证密钥", "placeholderEn": "Paste a 1–520 byte verifying key as hex", "helpZh": "必须来自可信电路构建流程;Studio 不会让 AI 生成验证密钥。", "helpEn": "Obtain this from the trusted circuit setup; Studio never asks AI to invent a verifying key." }, + { "id": "publicInput0", "type": "sha256", "argIndex": 1, "required": true, "labelZh": "公共输入 1", "labelEn": "Public input 1", "helpZh": "五个输入的顺序必须与电路完全一致。", "helpEn": "The five inputs must follow the circuit's exact order." }, + { "id": "publicInput1", "type": "sha256", "argIndex": 2, "required": true, "labelZh": "公共输入 2", "labelEn": "Public input 2" }, + { "id": "publicInput2", "type": "sha256", "argIndex": 3, "required": true, "labelZh": "公共输入 3", "labelEn": "Public input 3" }, + { "id": "publicInput3", "type": "sha256", "argIndex": 4, "required": true, "labelZh": "公共输入 4", "labelEn": "Public input 4" }, + { "id": "publicInput4", "type": "sha256", "argIndex": 5, "required": true, "labelZh": "公共输入 5", "labelEn": "Public input 5" }, + { "id": "recipientAddress", "type": "address", "argIndex": 6, "required": true, "labelZh": "固定收款钱包", "labelEn": "Fixed recipient wallet", "placeholderZh": "kaspatest:...", "placeholderEn": "kaspatest:...", "helpZh": "任何人都可以提交有效证明,但资金只能进入这个钱包。", "helpEn": "Anyone may submit a valid proof, but funds can only reach this wallet." } + ], + "constructorArgs": [ + { "kind": "byte[]", "data": [1] }, + { "kind": "bytes32", "hex": "1111111111111111111111111111111111111111111111111111111111111111" }, + { "kind": "bytes32", "hex": "2222222222222222222222222222222222222222222222222222222222222222" }, + { "kind": "bytes32", "hex": "3333333333333333333333333333333333333333333333333333333333333333" }, + { "kind": "bytes32", "hex": "4444444444444444444444444444444444444444444444444444444444444444" }, + { "kind": "bytes32", "hex": "5555555555555555555555555555555555555555555555555555555555555555" }, + { "kind": "pubkey", "hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" } + ], + "invariants": [ + "The Groth16 verifier uses the exact constructor-bound verifying key and five ordered public inputs", + "The operation has one covenant input and one fixed P2PK recipient output", + "The recipient receives input value minus a positive fee capped at 0.1 KAS/TKAS", + "No signature or AI decision can substitute for a valid proof" + ], + "transactionPlans": [ + { "transition": "claim", "inputs": [{ "role": "proof-locked-funds", "index": 0 }], "outputs": [{ "role": "fixed-recipient", "index": 0 }], "conservationChecks": ["g16.verify accepts the proof", "input = recipient output + explicit fee"] } + ], + "example": { + "titleZh": "可验证计算结果付款", + "titleEn": "Payment for a verifiable computation result", + "scenarioZh": "发布方锁定 TN10 资金并固定电路验证密钥、五个公共输入和收款钱包;提交者给出有效 Groth16 证明后触发付款。", + "scenarioEn": "A publisher locks TN10 funds and fixes a circuit verifying key, five public inputs and a recipient; a valid Groth16 proof releases the payment.", + "rolesZh": ["发布方:核对电路、验证密钥和五个公共输入后部署", "证明方:在本地生成证明并提交十六进制证明数据", "收款方:由构造参数固定,不能在领取时更换"], + "rolesEn": ["Publisher: reviews the circuit, verifying key and five public inputs before deployment", "Prover: generates the proof locally and submits its hex encoding", "Recipient: fixed at construction and cannot be replaced during claiming"], + "stepsZh": ["从可信电路工具导出压缩验证密钥和五个有序公共输入", "在 TN10 部署并核对编译器提交和程序哈希", "生成证明,在操作中心构建 claim", "本地脚本引擎验证通过后再广播"], + "stepsEn": ["Export the compressed verifying key and five ordered public inputs from a trusted circuit tool", "Deploy on TN10 and verify compiler commit and program hash", "Generate the proof and build claim in Operation Center", "Broadcast only after the local script engine verifies it"], + "resultZh": "链上脚本验证证明;应用、网站和 AI 都没有放行资金的权力。", + "resultEn": "The on-chain script verifies the proof; the app, website and AI cannot authorize release.", + "cautionZh": "这是未经独立审计的 TN10 研究模板。验证密钥或公共输入配置错误会永久锁定资金。", + "cautionEn": "This is an unaudited TN10 research template. A wrong verifying key or public input can lock funds permanently." + } +} diff --git a/templates/hashlock-refund/manifest.json b/templates/hashlock-refund/manifest.json index 55f214c..6534d62 100644 --- a/templates/hashlock-refund/manifest.json +++ b/templates/hashlock-refund/manifest.json @@ -22,6 +22,10 @@ "requirementsEn": "Require a SHA-256 preimage and recipient signature, with a sender refund after timeout.", "risk": "advanced", "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "sender", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "sender" }, + { "role": "recipient", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "recipient" } + ], "requiredReplacements": [0, 1, 2, 3], "parameters": [ { "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." }, diff --git a/templates/inheritance-vault/manifest.json b/templates/inheritance-vault/manifest.json index 1e173e2..9185ea7 100644 --- a/templates/inheritance-vault/manifest.json +++ b/templates/inheritance-vault/manifest.json @@ -23,6 +23,10 @@ "risk": "advanced", "parameterEncodingVersion": 2, "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "owner", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "owner" }, + { "role": "inheritors", "profile": "p2pk-schnorr/v1", "cardinalityParameter": "inheritors", "constructorParameter": "inheritors" } + ], "requiredReplacements": [0, 1, 2, 3], "parameters": [ { diff --git a/templates/kcc721-experimental/manifest.json b/templates/kcc721-experimental/manifest.json index 88ef487..33b778f 100644 --- a/templates/kcc721-experimental/manifest.json +++ b/templates/kcc721-experimental/manifest.json @@ -7,7 +7,7 @@ "descriptionEn": "Guided Collection metadata, NFT identity and owner setup with a locally computed digest. Four covenants are included for TN10 research; ordinary deployment remains disabled.", "experimentalOnly": true, "networkAllowlist": ["tn10"], - "compilerProfileId": "latest-cb34aa5", + "compilerProfileId": "latest-6f9e078", "deploymentMode": "pack-only", "deploymentBlockedReason": "KCC721 is a four-contract TN10 experimental pack; its dedicated independently reviewed genesis/mint builder is not included yet, so standalone deployment is disabled", "requirementsEn": "Research UTXO-native NFTs whose immutable identity and current owner are enforced by a live covenant lineage.", diff --git a/templates/merkle-one-time-claim/manifest.json b/templates/merkle-one-time-claim/manifest.json index fa9c401..1210f24 100644 --- a/templates/merkle-one-time-claim/manifest.json +++ b/templates/merkle-one-time-claim/manifest.json @@ -7,10 +7,14 @@ "descriptionEn": "A claimant consumes the UTXO once with a committed Merkle proof and salt; the refund wallet recovers it after timeout. TN10 experimental only.", "experimentalOnly": true, "networkAllowlist": ["tn10"], - "compilerProfileId": "latest-cb34aa5", + "compilerProfileId": "latest-6f9e078", "requirementsEn": "Allow exactly one claimant to consume a Merkle-committed claim before a timeout, with a signed refund path afterwards.", "risk": "experimental", "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "claimant", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "claimant" }, + { "role": "refund", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "refundKey" } + ], "requiredReplacements": [0, 1, 2, 3, 4, 5], "parameters": [ { "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." }, diff --git a/templates/owner-vault/manifest.json b/templates/owner-vault/manifest.json index 8a3a627..6a6eb7a 100644 --- a/templates/owner-vault/manifest.json +++ b/templates/owner-vault/manifest.json @@ -22,6 +22,9 @@ "requirementsEn": "Lock funds so only the configured owner key can spend them.", "risk": "starter", "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "owner", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "owner" } + ], "requiredReplacements": [0], "parameters": [ { "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." }, diff --git a/templates/timelock-transfer/manifest.json b/templates/timelock-transfer/manifest.json index 90a38a5..30a97db 100644 --- a/templates/timelock-transfer/manifest.json +++ b/templates/timelock-transfer/manifest.json @@ -22,6 +22,10 @@ "requirementsEn": "Allow the recipient to claim or the sender to refund after an absolute timeout.", "risk": "starter", "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "sender", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "sender" }, + { "role": "recipient", "profile": "p2pk-schnorr/v1", "cardinality": 1, "constructorParameter": "recipient" } + ], "requiredReplacements": [0, 1, 2], "parameters": [ { "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." }, diff --git a/templates/two-of-three/manifest.json b/templates/two-of-three/manifest.json index bff9e48..80f2fdd 100644 --- a/templates/two-of-three/manifest.json +++ b/templates/two-of-three/manifest.json @@ -22,6 +22,9 @@ "requirementsEn": "Require any two of three configured Schnorr keys to approve spending.", "risk": "starter", "sourceFile": "contract.sil", + "controlPrincipals": [ + { "role": "members", "profile": "p2pk-schnorr/v1", "cardinality": 3, "constructorParameter": "key1,key2,key3" } + ], "requiredReplacements": [0, 1, 2], "parameters": [ { "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." }, diff --git a/test/studio.test.mjs b/test/studio.test.mjs index 48f0927..6a978c9 100644 --- a/test/studio.test.mjs +++ b/test/studio.test.mjs @@ -95,6 +95,7 @@ function configuredTemplateParameters(template, network = "testnet-10") { if (field.type === "address") return [field.id, new kaspa.XOnlyPublicKey(TEMPLATE_KEYS[addressIndex++ % TEMPLATE_KEYS.length]).toAddress(network).toString()]; if (field.type === "datetime") return [field.id, "2035-01-02T03:04:00.000Z"]; if (field.type === "sha256") return [field.id, "42".repeat(32)]; + if (field.type === "hexBytes") return [field.id, "42".repeat(Math.max(1, Number(field.minimumBytes || 1)))]; if (field.type === "choice") return [field.id, String(field.default || field.options?.[0]?.value || "")]; if (field.type === "kcc721CollectionId") return [field.id, ""]; if (field.type === "kcc721Metadata") return [field.id, structuredClone(field.default || { name: "TN10 test NFT", attributes: [] })]; @@ -680,26 +681,26 @@ contract Compatibility(pubkey owner) { } }`; const profiles = compilerProfiles(); - assert.deepEqual(profiles.map((profile) => profile.id), ["latest-cb34aa5", "legacy-2a3961c"]); + assert.deepEqual(profiles.map((profile) => profile.id), ["latest-6f9e078", "latest-cb34aa5", "legacy-2a3961c"]); assert.ok(profiles.every((profile) => profile.configured)); - const report = detectBreakingChanges(`${legacySource}\n// checkSigFromStack and tx.inputs[0].outpointTransactionHash.reverse()`, "latest-cb34aa5"); + const report = detectBreakingChanges(`${legacySource}\n// checkSigFromStack and tx.inputs[0].outpointTransactionHash.reverse()`, "latest-6f9e078"); assert.equal(report.compatible, false); assert.ok(report.findings.some((finding) => finding.id === "entry-syntax" && finding.line === 3)); assert.ok(report.findings.some((finding) => finding.id === "reverse-removed" && finding.replacement === null)); - const migrated = migrateSourceToProfile(legacySource, "latest-cb34aa5"); + const migrated = migrateSourceToProfile(legacySource, "latest-6f9e078"); assert.deepEqual(migrated.applied, ["entry-syntax"]); assert.equal(migrated.report.compatible, true); const owner = byteArray(new Uint8Array(32).fill(3)); const legacyArtifact = await compileContract({ source: legacySource, constructorArgs: [owner], compilerProfileId: "legacy-2a3961c" }); - const latestArtifact = await compileContract({ source: migrated.source, constructorArgs: [owner], compilerProfileId: "latest-cb34aa5" }); + const latestArtifact = await compileContract({ source: migrated.source, constructorArgs: [owner], compilerProfileId: "latest-6f9e078" }); assert.equal(legacyArtifact.compiler.artifactBytecodeField, "script"); assert.equal(latestArtifact.compiler.artifactBytecodeField, "bytecode"); - const encoded = encodeConstructorArgsForProfile(migrated.source, [owner], "latest-cb34aa5"); + const encoded = encodeConstructorArgsForProfile(migrated.source, [owner], "latest-6f9e078"); assert.equal(encoded[0].data.type_ref.base, "byte"); assert.deepEqual(encoded[0].data.type_ref.array_dims, [{ kind: "fixed", value: 32 }]); }); -test("latest compiler profile detects and enforces cb34aa5 hardening changes", async () => { +test("latest compiler profile detects cb34aa5 hardening and 6f9e078 scalar conversions", async () => { const duplicate = `pragma silverscript ^0.1.0; contract Duplicate() { function same() { require(true); } @@ -714,12 +715,15 @@ contract Shadow() { contract Ordered() { entry spend() { require("b" > "a"); } }`; - assert.ok(detectBreakingChanges(duplicate, "latest-cb34aa5").findings.some((finding) => finding.id === "duplicate-function-name")); - assert.ok(detectBreakingChanges(shadow, "latest-cb34aa5").findings.some((finding) => finding.id === "entry-parameter-shadows-field")); - assert.ok(detectBreakingChanges(ordered, "latest-cb34aa5").findings.some((finding) => finding.id === "ordered-comparison-numeric-only")); - await assert.rejects(compileContract({ source: duplicate, compilerProfileId: "latest-cb34aa5" }), /duplicate function name/i); - await assert.rejects(compileContract({ source: shadow, compilerProfileId: "latest-cb34aa5" }), /conflicts with contract field/i); - await assert.rejects(compileContract({ source: ordered, compilerProfileId: "latest-cb34aa5" }), /ordered comparison requires numeric operands/i); + assert.ok(detectBreakingChanges(duplicate, "latest-6f9e078").findings.some((finding) => finding.id === "duplicate-function-name")); + assert.ok(detectBreakingChanges(shadow, "latest-6f9e078").findings.some((finding) => finding.id === "entry-parameter-shadows-field")); + assert.ok(detectBreakingChanges(ordered, "latest-6f9e078").findings.some((finding) => finding.id === "ordered-comparison-numeric-only")); + const conversions = detectBreakingChanges(`contract Cast() { entry run(int value, byte witness) { byte b = byte(value); int i = int(witness); } }`, "latest-6f9e078"); + assert.ok(conversions.findings.some((finding) => finding.id === "explicit-runtime-int-to-byte")); + assert.ok(conversions.findings.some((finding) => finding.id === "explicit-byte-signedness")); + await assert.rejects(compileContract({ source: duplicate, compilerProfileId: "latest-6f9e078" }), /duplicate function name/i); + await assert.rejects(compileContract({ source: shadow, compilerProfileId: "latest-6f9e078" }), /conflicts with contract field/i); + await assert.rejects(compileContract({ source: ordered, compilerProfileId: "latest-6f9e078" }), /ordered comparison requires numeric operands/i); }); test("CovenantStateSource rejects false matches, records fallback and fails on ambiguity", async () => { @@ -784,6 +788,7 @@ test("external covenant packages bind the P2SH program, covenant id, ABI slot, f assert.equal(inspected.review.covenantId, covenantId); assert.equal(inspected.review.feeSompi, "1000"); assert.equal(inspected.review.signatureSlots[0].signed, false); + assert.equal(inspected.review.descriptorStatus, "legacy-missing"); const exported = exportExternalCovenantPackage(packageValue, path.join(directory, "downloads")); assert.match(exported.filename, /^silverscript-[0-9a-f]{12}\.ssinvite$/); assert.deepEqual(JSON.parse(fs.readFileSync(exported.file, "utf8")), inspected.package); @@ -862,6 +867,15 @@ contract AtomicCell() { entry spend() { require(true); } }`; assert.deepEqual(inspected.review.targetInputIndexes, [0, 1]); assert.equal(inspected.review.p2pkAuthorization.signed, false); assert.equal(inspected.review.complete, false); + assert.equal(inspected.review.descriptorStatus, "verified-v1"); + assert.equal(inspected.package.networkCaip2, "kaspa:testnet-10"); + assert.ok(inspected.package.covenantInputs.every((item) => /^[0-9a-f]{64}$/.test(item.descriptorSha256))); + const forgedDescriptor = structuredClone(pkg); + forgedDescriptor.covenantInputs[0].descriptor.abi.sha256 = "00".repeat(32); + assert.throws(() => inspectExternalCovenantPackage(forgedDescriptor), /ABI commitment does not match/i); + const caipAlias = structuredClone(pkg); + caipAlias.network = "kaspa:testnet-10"; + assert.equal(inspectExternalCovenantPackage(caipAlias).review.network, "tn10"); const signed = await signP2pkCoSpendPackage({ package: inspected.package, walletId: created.wallet.id, @@ -1016,7 +1030,7 @@ test("TN10 Experimental KCC721 pack compiles all pinned contracts and blocks sta for (const contract of pack.packContracts) { const artifact = await compileContract({ source: contract.source, constructorArgs: contract.constructorArgs, compilerProfileId: pack.compilerProfileId }); assert.ok(artifact.programHex.length > 0, contract.id); - assert.equal(artifact.compiler.id, "latest-cb34aa5"); + assert.equal(artifact.compiler.id, "latest-6f9e078"); compiled.set(contract.id, artifact); } const configured = templates.projectInput(pack.id, "tn10", configuredTemplateParameters(pack)); @@ -1156,7 +1170,8 @@ test("every built-in template exposes a deterministic reverse operation package" ["timelock-transfer", { operationId: "claim", feeKas: "0.01" }], ["two-of-three", { operationId: "spend", destinationAddress: destination, signerAddresses: null, feeKas: "0.01" }], ["hashlock-refund", { operationId: "refund", feeKas: "0.01" }], - ["inheritance-vault", { operationId: "checkIn", feeKas: "0.01" }] + ["inheritance-vault", { operationId: "checkIn", feeKas: "0.01" }], + ["groth16-proof-release", { operationId: "claim", proofHex: "42", feeKas: "0.01" }] ]; for (const [templateId, operationInput] of cases) { const template = templates.get(templateId); @@ -1203,8 +1218,12 @@ test("every built-in template exposes a deterministic reverse operation package" assert.equal(built.review.covenantId, covenantId, templateId); const operations = templateOperations(project); assert.ok(operations.length >= 1, 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); + const proofOnly = templateId === "groth16-proof-release"; + assert.equal(built.review.feeSompi, templateId === "two-of-three" ? "1500000" : proofOnly ? "1000000" : "1250000", templateId); + assert.equal(built.fee.signatureExecutionReserveSompi, templateId === "two-of-three" ? "500000" : proofOnly ? "0" : "250000", templateId); + assert.equal(built.review.descriptorStatus, "verified-v1", templateId); + assert.match(built.review.descriptorSha256, /^[0-9a-f]{64}$/, templateId); + assert.ok(built.package.covenantInput.descriptor.controlPrincipals.length >= 1, templateId); assert.equal(built.preflight.verdict, "ready", templateId); if (templateId === "two-of-three") { assert.deepEqual(operations[0].availableSigners, [