From 81d401f89eeaaea413e6bc08dab5329ef5c41629 Mon Sep 17 00:00:00 2001 From: w00c00 <46839097+w00c00@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:07:11 +0800 Subject: [PATCH 1/2] feat: update SilverScript Studio to 0.2.9 --- .github/workflows/release-desktop.yml | 3 +- CHANGELOG.md | 20 ++++ README.md | 18 ++-- config/compiler-profiles.json | 86 +++++++++++++++--- docs/kcc721-experimental.md | 6 +- docs/studio-0.2-architecture.md | 4 +- docs/tn10-cross-covenant-fusion.md | 47 ++++++++++ docs/x402-experimental-profile.md | 27 ++++-- .../references/official-baseline.md | 19 ++-- .../references/upstream.json | 4 +- package-lock.json | 4 +- package.json | 2 +- scripts/build-silverc.mjs | 35 +++++-- scripts/prepare-desktop-runtime.mjs | 2 +- server/atomic-covenant-builder.mjs | 54 +++++++++-- server/compiler.mjs | 6 +- server/config.mjs | 16 +++- server/kcc20-security.mjs | 78 ++++++++++++++++ server/project-store.mjs | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 8 +- src-tauri/tauri.conf.json | 2 +- src/kcc721-metadata.js | 10 ++ src/main.js | 9 +- templates/commit-reveal/manifest.json | 2 +- templates/groth16-proof-release/manifest.json | 2 +- templates/kcc721-experimental/manifest.json | 4 +- templates/merkle-one-time-claim/manifest.json | 2 +- test/studio.test.mjs | 91 ++++++++++++++++--- 30 files changed, 472 insertions(+), 95 deletions(-) create mode 100644 docs/tn10-cross-covenant-fusion.md create mode 100644 server/kcc20-security.mjs diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 9e8a17c..6ec2500 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -113,7 +113,8 @@ jobs: PORT=4310 \ STUDIO_DATA_DIR="$data_dir" \ SILVERC_LATEST_BIN="$runtime/bin/silverc-latest" \ - SILVERC_PREVIOUS_BIN="$runtime/bin/silverc-cb34aa5" \ + SILVERC_PREVIOUS_BIN="$runtime/bin/silverc-6f9e078" \ + SILVERC_OLDER_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 6e0e40b..5de3068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.2.9 — 2026-08-15 + +- Updated the default SHA-256-pinned official SilverScript compiler to `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`; retained `6f9e078`, `cb34aa5`, and `2a3961c` as isolated reproducibility profiles. +- Added compatibility findings for declaration shadowing, `.split()` result typing, immutable `for` iterators, and the 10,000-iteration compiler limit introduced by the latest hardening changes. +- Added KCC20 security triage for borrowed-receive paths, including minter-leader rejection, token-state invariants, KAS-value preservation, pre-signed outpoint invalidation, and co-spend consent review. +- Extended the atomic multi-Covenant builder with explicit fresh-lineage outputs authorized by a verified input Covenant ID; genesis bindings are populated by the Kaspa transaction library and recorded in the operation package. +- Aligned KCC721 metadata with the KCC-0021 draft image-integrity rule: HTTPS images require a committed SHA-256, while IPFS URIs remain content-addressed. +- Updated the TN10 x402 interoperability profile to `kaspa-x402@alpha.10` (`78f2ada`) and documented stable `kaspa-escrow-v2` lineage plus partial-claim, top-up, and refund semantics without promoting it to a deployable built-in template. +- Documented the community TN10 cross-Covenant fusion transaction as evidence only; unavailable source and reproducibility data remain an explicit admission blocker. + +## 0.2.9 — 2026-08-15(中文) + +- 默认官方 SilverScript 编译器升级并固定到 `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`;隔离保留 `6f9e078`、`cb34aa5` 和 `2a3961c` 复现档案。 +- 增加最新编译器强化变更的兼容提示:声明名称遮蔽、`.split()` 结果类型、不可修改的 `for` 迭代变量,以及 10,000 次循环上限。 +- 增加 KCC20 安全初筛:检查借用接收路径的 minter leader 拒绝、Token 状态不变量、KAS value 保持、预签 outpoint 失效风险和共同输入授权语义。 +- 原子多 Covenant 构建器支持由已核验输入 Covenant ID 授权的新 lineage 输出;由 Kaspa 交易库填充 genesis binding,并将结果写入操作包。 +- KCC721 元数据采用 KCC-0021 Draft 图片完整性规则:HTTPS 图片必须提交 SHA-256,IPFS URI 继续依赖内容寻址。 +- TN10 x402 互操作档案升级到 `kaspa-x402@alpha.10`(`78f2ada`),记录稳定的 `kaspa-escrow-v2` lineage 与部分领取、追加和退款语义,但仍不伪装成可部署内置模板。 +- 将社区 TN10 跨 Covenant fusion 交易作为链上证据记录;缺少源码与可复现数据仍是正式准入阻断项。 + ## 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. diff --git a/README.md b/README.md index 71a11a3..d7729ff 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 - 中英文界面;首次启动自动读取系统语言,并在系统语言既非中文也非英文时使用时区辅助判断。 - 用户手动切换语言后,本机选择优先于自动识别。 - 本地项目工作区,可创建、切换和删除未使用的工作。 -- 三编译器兼容档案:默认固定 `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5`,保留 Studio 0.2.7 使用的 `cb34aa5` 和 `2a3961c` 旧版用于复现;三个二进制都校验 SHA-256。 -- 内置破坏性变更扫描与安全迁移,识别 `entry`、`checkMsgSig`、`outpointTxId`、artifact `bytecode` 和显式标量 byte/int 转换;无法安全自动迁移的 signed/unsigned 语义、`.reverse()` 和位运算会要求人工审查。 +- 四编译器兼容档案:默认固定 `kaspanet/silverscript@14dce9a5ce8769cdfbd0c8965f8764fa9c325067`,隔离保留 Studio 0.2.8 的 `6f9e078`、Studio 0.2.7 的 `cb34aa5` 和 `2a3961c` 旧版用于复现;所有二进制都校验 SHA-256。 +- 内置破坏性变更扫描与安全迁移,覆盖 `entry`、`checkMsgSig`、`outpointTxId`、artifact `bytecode`、标量转换、声明遮蔽、`.split()` 类型、不可变循环变量和循环上限;无法安全自动迁移的语义会要求人工审查。 - 固定 Kascov 来源提交构建的本地交易预检引擎。 - 支持 OpenAI、Anthropic、Gemini、OpenRouter、Ollama 和 OpenAI-compatible 接口。 - AI API Key 使用 scrypt 派生密钥和 AES-256-GCM 加密保存在本机。 @@ -47,7 +47,8 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 - 支持 `.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 输入。 +- 通用 P2PK co-spend 授权只签指定普通钱包输入,并锁定整笔交易承诺;原子构建器支持 2–32 个不同 Covenant 输入、同 lineage 延续和由已核验输入授权的新 lineage 输出。 +- KCC20 安全初筛覆盖借用接收、minter leader、Token 状态不变量、KAS value 保持、预签 outpoint 失效与共同输入授权边界。 - Kascov 是首选可视化和第二份报告来源,但不是签名、预检或广播的运行依赖。 ### 内置模板 @@ -224,7 +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)。 +编译器升级、状态查询与原子授权接口见 [Studio 0.2 架构说明](docs/studio-0.2-architecture.md)。KCC721 包的来源、边界和禁止事项见 [TN10 Experimental KCC721](docs/kcc721-experimental.md)。社区跨 Covenant 交易证据与新 lineage 构建格式见 [TN10 跨 Covenant Fusion 说明](docs/tn10-cross-covenant-fusion.md)。 Kaspa x402 的网络标识、操作包映射和正式模板准入条件见 [TN10 Experimental x402 档案](docs/x402-experimental-profile.md)。 ### 网络 @@ -293,8 +294,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. -- 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. +- Four isolated compiler profiles: the default is pinned to `kaspanet/silverscript@14dce9a5ce8769cdfbd0c8965f8764fa9c325067`; Studio 0.2.8's `6f9e078`, Studio 0.2.7's `cb34aa5`, and legacy `2a3961c` remain reproducible, with SHA-256 verification for every binary. +- Built-in breaking-change detection and safe migration cover `entry`, `checkMsgSig`, `outpointTxId`, artifact `bytecode`, scalar conversions, declaration shadowing, `.split()` typing, immutable loop iterators, and bounded-loop limits; ambiguous semantic 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. @@ -304,7 +305,8 @@ AI is limited to candidate generation and review assistance. It cannot unlock wa - 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. +- Generic isolated P2PK co-spend authorization plus an atomic builder for 2–32 distinct Covenant inputs, same-lineage continuations, and fresh lineages authorized by verified input Covenant IDs. +- KCC20 security triage covers borrowed receive, minter leaders, token-state invariants, KAS-value preservation, pre-signed outpoint invalidation, and co-spend consent boundaries. - Kascov is the preferred visual and secondary-report layer, not a signing, preflight, or broadcast dependency. ### Built-in templates @@ -455,7 +457,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 [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 [TN10 cross-Covenant fusion](docs/tn10-cross-covenant-fusion.md) for community transaction evidence and the fresh-lineage build format. 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 c13eda3..8dc2397 100644 --- a/config/compiler-profiles.json +++ b/config/compiler-profiles.json @@ -1,17 +1,37 @@ { - "defaultProfileId": "latest-6f9e078", + "defaultProfileId": "latest-14dce9a", "profiles": [ { - "id": "latest-6f9e078", - "label": "SilverScript latest (6f9e078)", - "upstreamCommit": "6f9e078b1d8b5389212755183b592704de99fea5", + "id": "latest-14dce9a", + "label": "SilverScript latest (14dce9a)", + "upstreamCommit": "14dce9a5ce8769cdfbd0c8965f8764fa9c325067", "binary": "bin/silverc-latest", "artifactBytecodeField": "bytecode", "syntaxGeneration": 2, "status": "experimental", "networkPolicy": "tn10-only", + "releasedAt": "2026-08-13", + "notes": [ + "Includes c1ed163 entrypoint, constant-cycle and bounded-loop hardening", + "Rejects declaration shadowing and attempts to override compiler builtins", + "Makes struct assignment atomic and struct-field lowering collision resistant", + "Fixes ternary lowering, struct append, bool array normalization and split result typing", + "Requires fixed byte arrays of at most eight bytes for int(byte[N]) casts", + "Keeps loop iteration variables immutable" + ] + }, + { + "id": "latest-6f9e078", + "label": "SilverScript Studio 0.2.8 (6f9e078)", + "upstreamCommit": "6f9e078b1d8b5389212755183b592704de99fea5", + "binary": "bin/silverc-6f9e078", + "artifactBytecodeField": "bytecode", + "syntaxGeneration": 2, + "status": "previous", + "networkPolicy": "tn10-only", "releasedAt": "2026-08-10", "notes": [ + "Retained to reproduce Studio 0.2.8 projects", "Uses entry syntax for public entrypoints", "Exposes transaction introspection as outpointTxId", "Uses checkMsgSig for arbitrary-message signatures", @@ -25,7 +45,7 @@ }, { "id": "latest-cb34aa5", - "label": "SilverScript previous (cb34aa5)", + "label": "SilverScript Studio 0.2.7 (cb34aa5)", "upstreamCommit": "cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "binary": "bin/silverc-cb34aa5", "artifactBytecodeField": "bytecode", @@ -60,7 +80,7 @@ "severity": "error", "introducedBy": "0f99803", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\bentrypoint\\s+function\\b", "messageZh": "公开入口语法已从 entrypoint function 改为 entry。", "messageEn": "Public entrypoint syntax changed from entrypoint function to entry.", @@ -71,7 +91,7 @@ "severity": "error", "introducedBy": "782a4d7", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\bcheckSigFromStack\\b", "messageZh": "任意消息签名内建函数已改名为 checkMsgSig。", "messageEn": "The arbitrary-message signature builtin was renamed to checkMsgSig.", @@ -82,7 +102,7 @@ "severity": "error", "introducedBy": "65421cf", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\.outpointTransactionHash\\b", "messageZh": "输入 outpoint 字段已改名为 outpointTxId。", "messageEn": "The input outpoint field was renamed to outpointTxId.", @@ -93,7 +113,7 @@ "severity": "error", "introducedBy": "6869e7d", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\.reverse\\s*\\(", "messageZh": ".reverse() 已删除,必须显式重写字节顺序逻辑并重新测试。", "messageEn": ".reverse() was removed; rewrite byte ordering explicitly and retest it.", @@ -104,7 +124,7 @@ "severity": "integration", "introducedBy": "4d88ded", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": null, "messageZh": "编译产物 JSON 字段由 script 改为 bytecode;外部工具必须兼容两个字段。", "messageEn": "The compiler artifact JSON field changed from script to bytecode; integrations must support both.", @@ -115,11 +135,55 @@ "severity": "manual-review", "introducedBy": "8b74812", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "(?:^|[^&|])(?:<<|>>|\\^|&|\\|)(?:[^&|]|$)", "messageZh": "位运算现在仅接受 bytes;请人工确认操作数类型。", "messageEn": "Bitwise operators now accept bytes only; review operand types manually.", "replacement": null + }, + { + "id": "declaration-shadowing-rejected", + "severity": "manual-review", + "introducedBy": "14dce9a", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": null, + "messageZh": "编译器现在禁止构造参数、字段、常量、函数参数和局部变量之间的名称遮蔽;请通过完整编译定位并重命名冲突声明。", + "messageEn": "The compiler now rejects name shadowing across constructor parameters, fields, constants, function parameters and locals; fully compile and rename conflicting declarations.", + "replacement": null + }, + { + "id": "split-result-typing", + "severity": "manual-review", + "introducedBy": "14dce9a", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": "\\.split\\s*\\(", + "messageZh": ".split() 结果现在会推断固定左侧长度;请复核显式类型、析构赋值和下游状态编码。", + "messageEn": ".split() now infers a fixed left-side length; review explicit types, tuple destructuring and downstream state encoding.", + "replacement": null + }, + { + "id": "for-iterator-immutable", + "severity": "error", + "introducedBy": "14dce9a", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": "for\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)[^\\n]*\\)[\\s\\S]{0,500}?\\b\\1\\s*(?:=|\\+=|-=|\\+\\+|--)", + "messageZh": "for 循环迭代变量现在不可修改;请使用独立局部变量表达循环内状态。", + "messageEn": "For-loop iteration variables are now immutable; use a separate local for mutable loop state.", + "replacement": null + }, + { + "id": "bounded-loop-limit", + "severity": "manual-review", + "introducedBy": "c1ed163", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": "max_iterations\\s*=\\s*(?:1000[1-9]|100[1-9][0-9]|10[1-9][0-9]{2}|1[1-9][0-9]{3}|[2-9][0-9]{4,})", + "messageZh": "编译器将 max_iterations 限制为 10,000;请缩小循环边界并重新评估脚本单位。", + "messageEn": "The compiler caps max_iterations at 10,000; reduce the bound and reassess script units.", + "replacement": null } ] } diff --git a/docs/kcc721-experimental.md b/docs/kcc721-experimental.md index 7b65a3b..e373524 100644 --- a/docs/kcc721-experimental.md +++ b/docs/kcc721-experimental.md @@ -6,8 +6,9 @@ Studio 内置的 KCC721 包改编自 `KaspaHUB21/KCC721` v0.2 社区草案,保 本包只允许 `tn10`,风险等级为 `high-experimental`。普通单 Covenant 部署路径被明确禁用,因为 Collection/Ticket/NFT 创世需要专用的多合约 builder 正确计算模板片段、模板哈希、Covenant ID 和 output binding。当前完成的是: -- 四份源码在官方 `silverc@6f9e078` 下完整编译。 +- 四份源码在官方 `silverc@14dce9a` 下完整编译。 - 模板使用三步配置向导,不再要求用户手填元数据摘要。名称、描述、图片 URI、外部链接和属性会先规范化为确定性 JSON,再由前后端分别计算并核对 SHA-256。 +- 元数据采用 KCC-0021 Draft 的图片完整性规则:`https://` 图片必须附带 32-byte SHA-256,`ipfs://` 由 CID 提供内容寻址;KCC-0021 尚未合并,界面继续标为 Draft/Experimental。 - “新集合”明确标记为编译预览,内部使用不可部署的全零哨兵;只有“导入已有 TN10 集合”模式接受从真实创世输出核验的 Collection Covenant ID。 - NFT 所有者变更绑定独立 P2PK co-spend 输入。 - 原子 builder 支持 2–32 个不同 Covenant ID 的批量交易。 @@ -34,8 +35,9 @@ 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@6f9e078`. +- Full compilation of all four sources with official `silverc@14dce9a`. - 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. +- Metadata follows the KCC-0021 Draft image-integrity rule: `https://` images require a 32-byte SHA-256 while `ipfs://` is content-addressed by its CID. KCC-0021 is still unmerged, so the UI remains Draft/Experimental. - 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. - Atomic transactions across 2–32 distinct covenant IDs. diff --git a/docs/studio-0.2-architecture.md b/docs/studio-0.2-architecture.md index 6c8cc93..dab0667 100644 --- a/docs/studio-0.2-architecture.md +++ b/docs/studio-0.2-architecture.md @@ -4,7 +4,7 @@ ### 编译器兼容档案 -`config/compiler-profiles.json` 是可提交的兼容性清单,`config/compiler.json` 是本机生成的二进制路径、构建时间和 SHA-256 清单。默认档案固定官方 SilverScript `6f9e078b1d8b5389212755183b592704de99fea5`;`cb34aa5` 保留用于复现 Studio 0.2.7 项目,`2a3961c` 保留用于更早的旧版项目。 +`config/compiler-profiles.json` 是可提交的兼容性清单,`config/compiler.json` 是本机生成的二进制路径、构建时间和 SHA-256 清单。默认档案固定官方 SilverScript `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`;`6f9e078`、`cb34aa5` 和 `2a3961c` 隔离保留,用于复现 Studio 0.2.8、0.2.7 与更早项目。 升级检查会报告已知变化,并只自动替换无歧义的名称。`6f9e078` 要求标量 byte 转 int 时明确使用 `signed()` 或 `unsigned()`,运行时 int 转 byte 使用 `as byte`;这些语义不能自动猜测。`.reverse()` 删除、字节序、位运算类型和任何状态布局变化也必须人工审查。迁移后仍必须使用真实构造参数完整编译并进行对抗性交易测试。 @@ -54,7 +54,7 @@ Studio 新生成的 `.ssinvite` 为每个 Covenant 输入附带 canonical v1 des ### 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 `6f9e078b1d8b5389212755183b592704de99fea5`; `cb34aa5` reproduces Studio 0.2.7 projects and `2a3961c` remains for older legacy projects. +`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 `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`; isolated `6f9e078`, `cb34aa5`, and `2a3961c` profiles reproduce Studio 0.2.8, 0.2.7, and older projects. 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. diff --git a/docs/tn10-cross-covenant-fusion.md b/docs/tn10-cross-covenant-fusion.md new file mode 100644 index 0000000..ad98eab --- /dev/null +++ b/docs/tn10-cross-covenant-fusion.md @@ -0,0 +1,47 @@ +# TN10 cross-covenant read/write observation / TN10 跨 Covenant 读写观察 + +## Status / 状态 + +On 2026-08-14 a community transaction was accepted on TN10 that co-spent two +different covenant lineages, continued one input, and used the other input to +authorize a fresh covenant lineage output. This is useful evidence for atomic +read/write composition, but the forum post does not publish a reproducible +source repository or compiler artifact. Studio therefore records it as an +observation and builder regression pattern, not as a deployable template or a +standard. + +2026-08-14,社区展示了一笔已被 TN10 接受的交易:共同花费两个不同 Covenant +lineage,延续其中一个输入,并由另一个 Covenant 输入授权新的 lineage 输出。这为原子 +读写组合提供了有价值的链上证据,但论坛没有给出可复现源码仓库与编译产物。因此 Studio +只把它记录为观察样本与 builder 回归模式,不把它包装成可部署模板或正式标准。 + +## Recorded evidence / 已记录证据 + +- Transaction: `975dbb2ad064ef5b4339f98e464d5f64df6af84a43995465cb3c9927b24dcf35` +- Input A covenant ID: `8614b79030c6024e61f1757a44ac803ba8c9fe314f5779c68709a672c10acdb7` +- Input B covenant ID: `2a8a728d3629da290aa08762830ef09e45692ff775dc4bb2b3b5cb435c39bdd3` +- Fresh output covenant ID reported by the transaction author: + `0fc9ff63bc367db7b8adfe463502904e0eb79b20c771156bae8f977b1d8d1f0e` +- Explorer: +- Discussion: + +## Studio builder rule / Studio builder 规则 + +`buildAtomicCovenantPackage` now accepts a covenant output with +`genesisAuthorizerCovenantId`. The builder derives the authorizing input index +from the verified input covenant ID, calls the consensus SDK's +`populateGenesisCovenants`, and records the resulting covenant ID in the +portable package. A caller-supplied input index or caller-supplied fresh +covenant ID is rejected. + +`buildAtomicCovenantPackage` 现在允许 Covenant 输出声明 +`genesisAuthorizerCovenantId`。Builder 会从已核验的输入 Covenant ID 推导授权输入位置, +调用共识 SDK 的 `populateGenesisCovenants`,并把生成的 Covenant ID 写入操作包。调用方 +不能直接指定授权 input index,也不能预先伪造新 Covenant ID。 + +This only constructs and binds the transaction. Each participating redeem +program must still independently validate the intended cross-covenant state, +script, value, cardinality and output covenant identity. + +这只完成交易构建和绑定。参与交易的每个 redeem program 仍必须独立核验预期的跨 +Covenant 状态、脚本、金额、数量以及输出 Covenant identity。 diff --git a/docs/x402-experimental-profile.md b/docs/x402-experimental-profile.md index 6fad293..902e687 100644 --- a/docs/x402-experimental-profile.md +++ b/docs/x402-experimental-profile.md @@ -1,13 +1,24 @@ # Kaspa x402 TN10 experimental profile / Kaspa x402 TN10 实验档案 -Studio tracks the public Kaspa x402 alpha at source commit -`7cae0eeea174f4ed077c96f376af69a38d18eddb`. This is an interoperability +Studio tracks Kaspa x402 `v0.1.0-alpha.10` at the peeled source commit +`78f2ada2b2dd3d54116b4cbca1fe9ec08691efc0`. This is an interoperability profile, not a mainnet-readiness claim and not an authorization source. -Studio 跟踪公开的 Kaspa x402 alpha 源码提交 -`7cae0eeea174f4ed077c96f376af69a38d18eddb`。这是互操作档案,不代表主网成熟, +Studio 跟踪 Kaspa x402 `v0.1.0-alpha.10` 对应的源码提交 +`78f2ada2b2dd3d54116b4cbca1fe9ec08691efc0`。这是互操作档案,不代表主网成熟, 也不能作为资金授权来源。 +Alpha.10 keeps `standard-native` and `additive` exact payments and replaces the +previous batch design with `kaspa-escrow-v2`. The batch profile uses a stable +KIP-20 lineage, lifetime accounting, partial claims, same-lineage top-ups, +refunds, and an explicit claim reserve. Studio records these semantics for +portable-package interoperability only; the upstream release remains TN10-only. + +Alpha.10 保留 `standard-native` 与 `additive` 单次付款,并用 +`kaspa-escrow-v2` 替换旧批量方案。批量档案使用稳定 KIP-20 lineage、全生命周期 +记账、部分领取、同 lineage 充值、退款和显式领取预留。Studio 目前只把这些语义用于 +操作包互操作档案;上游版本仍仅限 TN10。 + ## Network identifiers / 网络标识 - Studio internal: `tn10` @@ -28,9 +39,9 @@ Kaspa node network. |---|---| | 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 | +| `kaspa-escrow-v2` deposit/top-up | stable KIP-20 lineage plus 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 | +| partial claim/refund | versioned `.ssinvite` package, lifetime-accounting checks and local engine preflight | | settlement evidence | node txid plus optional Kascov visualization | Every future executable x402 template must pin the upstream source and compiler, @@ -45,4 +56,6 @@ upstream alpha escrow as a deployable built-in template. 延续、退款、重放、错误网络、错误 outpoint、价值守恒和手续费边界测试。在这些门槛完成前, Studio 不会把上游 alpha 托管合约伪装成可直接部署的成熟内置模板。 -Primary source: +Primary source: + +Release: diff --git a/knowledge/kaspa-silverscript/references/official-baseline.md b/knowledge/kaspa-silverscript/references/official-baseline.md index 410bbba..a3c7516 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: `6f9e078b1d8b5389212755183b592704de99fea5` -- Verified date: 2026-08-10 +- Verified commit: `14dce9a5ce8769cdfbd0c8965f8764fa9c325067` +- Verified date: 2026-08-15 - 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 @@ -67,6 +67,11 @@ deployment, compatibility, or mainnet-readiness questions. 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. +- Commit `c1ed163` hardens entrypoint arguments and constant evaluation and caps + bounded-loop `max_iterations` at 10,000. +- Commit `14dce9a` rejects declaration shadowing and builtin overrides and fixes + struct assignment/lowering, ternary lowering, bool-array normalization, + `.split()` result typing, and scalar-cast validation. ## Terminology discipline diff --git a/knowledge/kaspa-silverscript/references/upstream.json b/knowledge/kaspa-silverscript/references/upstream.json index b5ae3fe..b61c754 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": "6f9e078b1d8b5389212755183b592704de99fea5", - "verified_at": "2026-08-10", + "verified_commit": "14dce9a5ce8769cdfbd0c8965f8764fa9c325067", + "verified_at": "2026-08-15", "status": "experimental", "recommended_network": "testnet-10" } diff --git a/package-lock.json b/package-lock.json index e4626fb..6388c5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.8", + "version": "0.2.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kaspa-silverscript-studio", - "version": "0.2.8", + "version": "0.2.9", "license": "MIT", "dependencies": { "@kluster/kaspa-wasm": "2.0.1", diff --git a/package.json b/package.json index 6b5b8fc..f47f8bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.8", + "version": "0.2.9", "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 8d46afb..f638c98 100644 --- a/scripts/build-silverc.mjs +++ b/scripts/build-silverc.mjs @@ -8,8 +8,9 @@ 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 = "6f9e078b1d8b5389212755183b592704de99fea5"; -const previousCommit = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; +const latestCommit = "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"; +const previousCommit = "6f9e078b1d8b5389212755183b592704de99fea5"; +const olderCommit = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; const legacyCommit = "2a3961cadc76bb16a425042172ffe32481da89b5"; const work = fs.mkdtempSync(path.join(os.tmpdir(), "silverstudio-silverc-")); @@ -36,6 +37,8 @@ function buildProfile({ id, commit, outputName, configuredSource }) { run("git", ["-C", repository, "checkout", "--detach", commit]); } + // Keep revision targets isolated. Cargo can otherwise reuse a same-name and + // same-version workspace artifact from a different SilverScript commit. const targetDirectory = path.join(work, `target-${id}`); run("cargo", [ "build", @@ -56,17 +59,23 @@ function buildProfile({ id, commit, outputName, configuredSource }) { try { fs.mkdirSync(path.join(root, "config"), { recursive: true }); const latestBin = buildProfile({ - id: "latest-6f9e078", + id: "latest-14dce9a", commit: latestCommit, outputName: "silverc-latest", configuredSource: process.env.SILVERSCRIPT_LATEST_SOURCE || process.env.SILVERSCRIPT_SOURCE || "" }); const previousBin = buildProfile({ - id: "latest-cb34aa5", + id: "latest-6f9e078", commit: previousCommit, - outputName: "silverc-cb34aa5", + outputName: "silverc-6f9e078", configuredSource: process.env.SILVERSCRIPT_PREVIOUS_SOURCE || "" }); + const olderBin = buildProfile({ + id: "latest-cb34aa5", + commit: olderCommit, + outputName: "silverc-cb34aa5", + configuredSource: process.env.SILVERSCRIPT_OLDER_SOURCE || "" + }); const legacyBin = buildProfile({ id: "legacy-2a3961c", commit: legacyCommit, @@ -75,22 +84,29 @@ try { }); const latestSha256 = sha256(latestBin); const previousSha256 = sha256(previousBin); + const olderSha256 = sha256(olderBin); const legacySha256 = sha256(legacyBin); const manifest = { - defaultProfileId: "latest-6f9e078", + defaultProfileId: "latest-14dce9a", profiles: { - "latest-6f9e078": { + "latest-14dce9a": { bin: latestBin, sha256: latestSha256, upstreamCommit: latestCommit, builtAt: new Date().toISOString() }, - "latest-cb34aa5": { + "latest-6f9e078": { bin: previousBin, sha256: previousSha256, upstreamCommit: previousCommit, builtAt: new Date().toISOString() }, + "latest-cb34aa5": { + bin: olderBin, + sha256: olderSha256, + upstreamCommit: olderCommit, + builtAt: new Date().toISOString() + }, "legacy-2a3961c": { bin: legacyBin, sha256: legacySha256, @@ -102,11 +118,14 @@ try { fs.writeFileSync(path.join(root, "config", "compiler.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 }); run(latestBin, ["--help"]); run(previousBin, ["--help"]); + run(olderBin, ["--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(`older silverc commit: ${olderCommit}`); + console.log(`older silverc sha256: ${olderSha256}`); 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 2c185df..090809f 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-cb34aa5", "silverc-legacy", "kascov-preflight"]; +const helperNames = ["silverc-latest", "silverc-6f9e078", "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 8ac0a36..93708ee 100644 --- a/server/atomic-covenant-builder.mjs +++ b/server/atomic-covenant-builder.mjs @@ -53,19 +53,34 @@ function outputFrom(descriptor, network, inputIndexById) { const value = BigInt(descriptor.valueSompi); if (value <= 0n) throw builderError("Every atomic output must have positive value"); if (descriptor.address) { + if (descriptor.genesisAuthorizerCovenantId) throw builderError("An address output cannot also be a fresh covenant lineage"); const address = String(descriptor.address).trim().toLowerCase(); if (!address.startsWith(`${network.prefix}:`)) throw builderError("Atomic output address is on the wrong network"); - return new kaspa.TransactionOutput(value, kaspa.payToAddressScript(address)); + return { output: new kaspa.TransactionOutput(value, kaspa.payToAddressScript(address)), genesisAuthorizingInput: null }; } const programHex = cleanHex(descriptor.programHex); + if (descriptor.genesisAuthorizerCovenantId) { + if (descriptor.covenantId) throw builderError("A fresh covenant lineage output must not declare a pre-existing covenant ID"); + const authorizerId = cleanHex(descriptor.genesisAuthorizerCovenantId, 32); + const authorizingInput = inputIndexById.get(authorizerId); + if (!Number.isSafeInteger(authorizingInput)) throw builderError("Fresh covenant lineage authorizer does not map to an input covenant ID"); + return { + output: new kaspa.TransactionOutput(value, kaspa.payToScriptHashScript(programHex)), + genesisAuthorizingInput: authorizingInput, + programSha256: sha256(Buffer.from(programHex, "hex")) + }; + } const covenantId = cleanHex(descriptor.covenantId, 32); const sourceInputIndex = inputIndexById.get(covenantId); if (!Number.isSafeInteger(sourceInputIndex)) throw builderError("Covenant continuation output does not map to an input covenant ID"); - return new kaspa.TransactionOutput( - value, - kaspa.payToScriptHashScript(programHex), - new kaspa.CovenantBinding(sourceInputIndex, new kaspa.Hash(covenantId)) - ); + return { + output: new kaspa.TransactionOutput( + value, + kaspa.payToScriptHashScript(programHex), + new kaspa.CovenantBinding(sourceInputIndex, new kaspa.Hash(covenantId)) + ), + genesisAuthorizingInput: null + }; } export function buildAtomicCovenantPackage({ network: networkId = "tn10", covenantInputs, outputs, p2pkAuthorization = null, feeSompi, provenance = {} }) { @@ -147,7 +162,8 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena transactionInputs.push(authInput); } const inputIndexById = new Map(metadata.map((item) => [item.covenantId, item.index])); - const transactionOutputs = outputs.map((output) => outputFrom(output, network, inputIndexById)); + const outputPlans = outputs.map((output) => outputFrom(output, network, inputIndexById)); + const transactionOutputs = outputPlans.map((plan) => plan.output); const inputTotal = covenantInputs.reduce((sum, item) => sum + amountOf(item.utxo), 0n) + (p2pkAuthorization?.input ? amountOf(p2pkAuthorization.input.utxo) : 0n); const outputTotal = outputs.reduce((sum, output) => sum + BigInt(output.valueSompi), 0n); @@ -163,6 +179,28 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena gas: 0n, payload: "" }); + const genesisGroups = new Map(); + outputPlans.forEach((plan, outputIndex) => { + if (!Number.isSafeInteger(plan.genesisAuthorizingInput)) return; + const indexes = genesisGroups.get(plan.genesisAuthorizingInput) || []; + indexes.push(outputIndex); + genesisGroups.set(plan.genesisAuthorizingInput, indexes); + }); + if (genesisGroups.size) { + transaction.populateGenesisCovenants([...genesisGroups].map(([authorizingInput, genesisOutputs]) => ({ + authorizingInput, + outputs: genesisOutputs + }))); + } + const genesisCovenants = outputPlans.flatMap((plan, outputIndex) => { + if (!Number.isSafeInteger(plan.genesisAuthorizingInput)) return []; + const covenantId = String(transaction.outputs[outputIndex].covenant?.covenantId || "").toLowerCase(); + if (!/^[0-9a-f]{64}$/.test(covenantId)) throw builderError("Fresh covenant lineage ID was not populated", "GENESIS_COVENANT_POPULATION_FAILED"); + if (Number(transaction.outputs[outputIndex].covenant?.authorizingInput) !== plan.genesisAuthorizingInput) { + throw builderError("Fresh covenant lineage has the wrong authorizing input", "GENESIS_COVENANT_AUTHORIZATION_MISMATCH"); + } + return [{ outputIndex, authorizingInputIndex: plan.genesisAuthorizingInput, covenantId, programSha256: plan.programSha256 }]; + }); const covenantSignatures = metadata.reduce((sum, item) => sum + (item.arguments || []).filter((argument) => argument?.kind === "signature").length, 0); const sigOps = Math.max(1, covenantSignatures + (p2pkAuthorization?.input ? 1 : 0)); if (!kaspa.updateTransactionMass(network.kaspaNetworkId, transaction, sigOps, true)) throw builderError("Atomic covenant transaction exceeds the current mass limit", "ATOMIC_MASS_LIMIT"); @@ -172,11 +210,13 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena networkCaip2: caip2Network(network.id), transactionSafeJson: transaction.serializeToSafeJSON(), covenantInputs: metadata, + ...(genesisCovenants.length ? { genesisCovenants } : {}), ...(p2pkAuthorization?.metadata ? { p2pkAuthorization: { ...p2pkAuthorization.metadata, signed: false } } : {}), provenance: { kind: "silverstudio-atomic-covenant", atomic: true, inputCount: metadata.length, + genesisOutputCount: genesisCovenants.length, ...provenance } }; diff --git a/server/compiler.mjs b/server/compiler.mjs index 34fcb97..853f0ef 100644 --- a/server/compiler.mjs +++ b/server/compiler.mjs @@ -6,6 +6,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { config } from "./config.mjs"; import { boundedText, sha256 } from "./security.mjs"; +import { analyzeKcc20Source } from "./kcc20-security.mjs"; const execFileAsync = promisify(execFile); @@ -65,7 +66,7 @@ function lineAt(source, offset) { } function latestHardeningFindings(source, target) { - if (!["cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "6f9e078b1d8b5389212755183b592704de99fea5"].includes(target.upstreamCommit)) return []; + if (!["cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "6f9e078b1d8b5389212755183b592704de99fea5", "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"].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; @@ -136,7 +137,7 @@ function latestHardeningFindings(source, target) { } function scalarConversionFindings(source, target) { - if (target.upstreamCommit !== "6f9e078b1d8b5389212755183b592704de99fea5") return []; + if (!["6f9e078b1d8b5389212755183b592704de99fea5", "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"].includes(target.upstreamCommit)) return []; const findings = []; const scalarByteCast = /\bbyte\s*\(\s*([^()\n]+?)\s*\)/g; let match; @@ -349,6 +350,7 @@ export async function staticAnalyze(source) { message: "review fee-output aliasing across multiple contract executions" }); } + findings.push(...analyzeKcc20Source(text)); return { kind: "heuristic-triage", findings, diff --git a/server/config.mjs b/server/config.mjs index 2233f12..f73c705 100644 --- a/server/config.mjs +++ b/server/config.mjs @@ -8,8 +8,9 @@ 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 = "6f9e078b1d8b5389212755183b592704de99fea5"; -export const SILVERSCRIPT_PREVIOUS_COMMIT = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; +export const SILVERSCRIPT_COMMIT = "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"; +export const SILVERSCRIPT_PREVIOUS_COMMIT = "6f9e078b1d8b5389212755183b592704de99fea5"; +export const SILVERSCRIPT_OLDER_COMMIT = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; export const SILVERSCRIPT_LEGACY_COMMIT = "2a3961cadc76bb16a425042172ffe32481da89b5"; export const NETWORKS = Object.freeze({ @@ -56,12 +57,17 @@ function loadCompilerConfig() { ? stored : {}; const isPrevious = definition.upstreamCommit === SILVERSCRIPT_PREVIOUS_COMMIT; + const isOlder = definition.upstreamCommit === SILVERSCRIPT_OLDER_COMMIT; const environmentBin = isLatest ? process.env.SILVERC_LATEST_BIN || process.env.SILVERC_BIN - : isPrevious ? process.env.SILVERC_PREVIOUS_BIN : process.env.SILVERC_LEGACY_BIN; + : isPrevious ? process.env.SILVERC_PREVIOUS_BIN || process.env.SILVERC_6F9E078_BIN + : isOlder ? process.env.SILVERC_OLDER_BIN || process.env.SILVERC_CB34AA5_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; + : isPrevious ? process.env.SILVERC_PREVIOUS_SHA256 || process.env.SILVERC_6F9E078_SHA256 + : isOlder ? process.env.SILVERC_OLDER_SHA256 || process.env.SILVERC_CB34AA5_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)), @@ -71,7 +77,7 @@ function loadCompilerConfig() { })); const defaultProfileId = profiles[stored.defaultProfileId] ? stored.defaultProfileId - : compatibility.defaultProfileId || "latest-6f9e078"; + : compatibility.defaultProfileId || "latest-14dce9a"; return Object.freeze({ defaultProfileId, profiles: Object.freeze(profiles), diff --git a/server/kcc20-security.mjs b/server/kcc20-security.mjs new file mode 100644 index 0000000..87fbc21 --- /dev/null +++ b/server/kcc20-security.mjs @@ -0,0 +1,78 @@ +function finding(code, severity, line, messageZh, messageEn) { + return { code, severity, line, messageZh, messageEn, message: messageEn }; +} + +function lineAt(source, offset) { + return source.slice(0, Math.max(0, offset)).split("\n").length; +} + +function hasAll(source, patterns) { + return patterns.every((pattern) => pattern.test(source)); +} + +export function analyzeKcc20Source(source) { + const text = String(source || ""); + const stateShape = [/\bownerIdentifier\b/, /\bidentifierType\b/, /\bamount\b/, /\bisMinter\b/]; + if (!hasAll(text, stateShape)) return []; + + const findings = []; + const borrowed = /\bBORROWED_RECEIVE\b|\bborrow(?:ed|ing)?\b/i.exec(text); + if (borrowed) { + const rejectsMinterLeader = /require\s*\(\s*!\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.isMinter\s*\)/.test(text); + if (!rejectsMinterLeader) { + findings.push(finding( + "KCC201", + "error", + lineAt(text, borrowed.index), + "检测到免签借用接收路径,但没有明确拒绝 minter leader;该组合可能让任意调用者保留铸币权并增发。", + "A borrowed-receive path is present without an explicit minter-leader rejection; an unauthenticated leader may retain mint authority and inflate supply." + )); + } + + const preservesOwner = /(?:newStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.ownerIdentifier\s*==\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.ownerIdentifier/.test(text); + const preservesType = /(?:newStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.identifierType\s*==\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.identifierType/.test(text); + const amountIncreases = /(?:newStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.amount\s*>\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.amount/.test(text); + if (!preservesOwner || !preservesType || !amountIncreases) { + findings.push(finding( + "KCC202", + "error", + lineAt(text, borrowed.index), + "借用接收必须保持 ownerIdentifier 和 identifierType 不变,并要求 token amount 严格增加。", + "Borrowed receive must preserve ownerIdentifier and identifierType and require the token amount to increase strictly." + )); + } + + const preservesKas = /OpCovOutputIdx\s*\([\s\S]{0,240}?\.value\s*>=\s*[\s\S]{0,160}?OpCovInputIdx/.test(text) + || /tx\.outputs\s*\[[^\]]+\]\.value\s*>=\s*tx\.inputs\s*\[[^\]]+\]\.value/.test(text); + if (!preservesKas) { + findings.push(finding( + "KCC203", + "error", + lineAt(text, borrowed.index), + "借用接收没有明显保持或增加被借用 UTXO 的 KAS value;这可能把接收者的存储押金转走。", + "Borrowed receive does not visibly preserve or increase the borrowed UTXO KAS value, which may drain the recipient's storage deposit." + )); + } + + findings.push(finding( + "KCC204", + "manual-review", + lineAt(text, borrowed.index), + "借用会生成新的 outpoint,令旧的预签交易失效。若需要可借用后继续授权,应使用带域分离、网络、Covenant ID、lineage、nonce、收款方和金额的 checkMsgSig 意图,并做重放测试。", + "Borrowing creates a new outpoint and invalidates old pre-signed transactions. Borrow-surviving authorization needs a replay-tested checkMsgSig intent bound to a domain, network, covenant ID, lineage, nonce, recipient and amount." + )); + } + + const presenceAuthorization = /IDENTIFIER_(?:SCRIPT_HASH|COVENANT_ID)[\s\S]{0,900}?(?:OpInputCovenantId|scriptPublicKey)/.exec(text); + if (presenceAuthorization) { + findings.push(finding( + "KCC205", + "manual-review", + lineAt(text, presenceAuthorization.index), + "共同输入的存在只能证明控制主体参与了交易,不能自动证明它专门同意这次 token 转移;请审查共同花费脚本是否承诺完整交易或明确意图。", + "A co-present control input proves participation, not specific consent to this token transfer; review whether its spend commits to the complete transaction or an explicit intent." + )); + } + + return findings; +} diff --git a/server/project-store.mjs b/server/project-store.mjs index 35a7089..599107c 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-6f9e078"), + compilerProfileId: String(input.compilerProfileId || "latest-14dce9a"), templateParameters: input.templateParameters && typeof input.templateParameters === "object" ? input.templateParameters : {}, deployAmount: String(input.deployAmount || "0.5"), specification: input.specification || null, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 40231b7..144d5f6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -66,7 +66,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "app" -version = "0.2.8" +version = "0.2.9" dependencies = [ "log", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a688aee..11ecb65 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.2.8" +version = "0.2.9" 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 cc00ee9..19054f3 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, PathBuf), String> { +fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf, PathBuf), String> { let resource_dir = app .path() .resource_dir() @@ -44,6 +44,7 @@ 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-6f9e078{executable_suffix}")), resource_dir.join(format!( "runtime/app/bin/silverc-cb34aa5{executable_suffix}" )), @@ -78,12 +79,13 @@ 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, previous_compiler, legacy_compiler, preflight_engine) = + let (script, latest_compiler, previous_compiler, older_compiler, legacy_compiler, preflight_engine) = runtime_paths(app)?; let required = [ &script, &latest_compiler, &previous_compiler, + &older_compiler, &legacy_compiler, &preflight_engine, ]; @@ -114,6 +116,7 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { 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 older_compiler_arg = child_process_path(&older_compiler); let legacy_compiler_arg = child_process_path(&legacy_compiler); let preflight_engine_arg = child_process_path(&preflight_engine); @@ -127,6 +130,7 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { .env("STUDIO_DATA_DIR", data_arg) .env("SILVERC_LATEST_BIN", latest_compiler_arg) .env("SILVERC_PREVIOUS_BIN", previous_compiler_arg) + .env("SILVERC_OLDER_BIN", older_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 40dfbcb..5c0ec5a 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.8", + "version": "0.2.9", "identifier": "io.kaspa.silverscript-studio", "build": { "frontendDist": "../dist", diff --git a/src/kcc721-metadata.js b/src/kcc721-metadata.js index b8ce751..f72951d 100644 --- a/src/kcc721-metadata.js +++ b/src/kcc721-metadata.js @@ -11,6 +11,12 @@ function cleanText(value, label, maximum, required = false) { return text; } +function cleanImageHash(value) { + const text = String(value ?? "").trim().toLowerCase().replace(/^0x/, ""); + if (text && !/^[0-9a-f]{64}$/.test(text)) metadataError("NFT image SHA-256 must contain exactly 64 hexadecimal characters"); + return text; +} + function stable(value) { if (Array.isArray(value)) return value.map(stable); if (!value || typeof value !== "object") return value; @@ -26,6 +32,9 @@ export function canonicalKcc721Metadata(value) { const name = cleanText(input.name, "NFT name", 120, true); const description = cleanText(input.description, "NFT description", 2_000); const image = cleanText(input.image, "NFT image URI", 2_048); + const imageHash = cleanImageHash(input.imageHash ?? input.image_hash); + if (image && !/^(?:https:\/\/|ipfs:\/\/)/i.test(image)) metadataError("NFT image URI must use https:// or ipfs://"); + if (/^https:\/\//i.test(image) && !imageHash) metadataError("HTTPS NFT images require an immutable image SHA-256"); const externalUrl = cleanText(input.externalUrl ?? input.external_url, "NFT external URL", 2_048); let attributes = input.attributes ?? []; if (typeof attributes === "string") { @@ -45,6 +54,7 @@ export function canonicalKcc721Metadata(value) { name, ...(description ? { description } : {}), ...(image ? { image } : {}), + ...(imageHash ? { image_hash: imageHash } : {}), ...(externalUrl ? { external_url: externalUrl } : {}), ...(attributes.length ? { attributes } : {}) }; diff --git a/src/main.js b/src/main.js index bcc10f5..0064a11 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-6f9e078", + compilerProfileId: $("#compiler-profile").value || state.config?.compiler?.defaultProfileId || "latest-14dce9a", 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-6f9e078"; + $("#compiler-profile").value = project.compilerProfileId || project.artifact?.compiler?.id || project.review?.compilerProfileId || state.config?.compiler?.defaultProfileId || "latest-14dce9a"; renderCompilerProfileHelp(); $("#deploy-amount").value = Number(project.deployAmount || 0) >= 0.5 ? project.deployAmount : "0.5"; $("#deploy-network").value = project.network || "tn10"; @@ -437,7 +437,7 @@ function initializeTemplateValues(template) { if (values[field.id] !== undefined && values[field.id] !== "") continue; if (field.type === "heirs") values[field.id] = [{ address: "", shareBps: 5000 }, { address: "", shareBps: 5000 }]; else if (field.type === "duration") values[field.id] = { value: String(field.defaultValue || 1), unit: field.defaultUnit || "days" }; - else if (field.type === "kcc721Metadata") values[field.id] = structuredClone(field.default || { name: "", description: "", image: "", externalUrl: "", attributes: [] }); + else if (field.type === "kcc721Metadata") values[field.id] = structuredClone(field.default || { name: "", description: "", image: "", imageHash: "", externalUrl: "", attributes: [] }); else if (field.default !== undefined) values[field.id] = String(field.default); else if (field.defaultOffsetSeconds) values[field.id] = localDateTimeValue(Date.now() + Number(field.defaultOffsetSeconds) * 1000); else if (field.type === "address" && field.useConnectedWallet && state.wallet?.network === $("#deploy-network").value) values[field.id] = state.wallet.address; @@ -490,6 +490,7 @@ function renderTemplateParameterFields(template) {