diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 235a2bd..aceb5c1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,37 @@ version: 2 updates: - - package-ecosystem: github-actions + - package-ecosystem: gomod directory: / schedule: interval: weekly day: monday time: "03:00" timezone: Asia/Shanghai + # Routine version PRs stay disabled so security fixes remain focused. + # GitHub applies a separate limit to Dependabot security updates. + open-pull-requests-limit: 0 + rebase-strategy: auto + labels: + - dependencies + - go + commit-message: + prefix: "fix(deps)" + allow: + - dependency-type: all + groups: + security-patches: + applies-to: security-updates + patterns: + - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "03:15" + timezone: Asia/Shanghai open-pull-requests-limit: 5 rebase-strategy: auto labels: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e4d85e0..dd4c86f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -38,6 +38,11 @@ For every unchecked box or affected boundary, explain the ADR, proof obligation, - [ ] `git diff --check` - [ ] `python3 scripts/check_docs.py` +- [ ] `go mod verify` and a clean `go mod tidy` diff +- [ ] `go vet ./...` +- [ ] `go test -count=1 -mod=readonly ./...` +- [ ] `go test -race -count=1 -mod=readonly ./...` +- [ ] `python3 scripts/check_go_architecture.py` - [ ] Relevant unit, race, property, fuzz, vector, model, Byzantine, partition, crash-recovery, snapshot, chaos, and performance checks - [ ] Negative tests cover malformed, conflicting, stale, replayed, oversized, and partially durable inputs where applicable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e3a505..1b93c2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,3 +114,143 @@ jobs: - name: Validate documentation tree and local links run: python3 scripts/check_docs.py + + go-quality: + name: Go quality + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Check formatting + shell: bash + run: | + unformatted="$(gofmt -l $(git ls-files '*.go'))" + if [ -n "$unformatted" ]; then + printf '%s\n' "$unformatted" + echo "::error::run gofmt on the listed files" + exit 1 + fi + + - name: Download dependencies + run: go mod download + + - name: Verify module dependencies + run: go mod verify + + - name: Check module tidiness + run: | + go mod tidy + git diff --exit-code -- go.mod go.sum + + - name: Run go vet + run: go vet ./... + + - name: Enforce Go architecture boundaries + run: python3 scripts/check_go_architecture.py + + go-unit: + name: Go unit tests + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Run unit tests + run: go test -count=1 -mod=readonly ./... + + go-race: + name: Go race tests + runs-on: ubuntu-24.04 + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Run race tests + run: go test -race -count=1 -mod=readonly ./... + + go-build: + name: Go build (linux/${{ matrix.arch }}) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + arch: + - amd64 + - arm64 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Enforce architecture boundaries for target + env: + GOOS: linux + GOARCH: ${{ matrix.arch }} + CGO_ENABLED: "0" + run: python3 scripts/check_go_architecture.py + + - name: Build finalweave-node + env: + GOOS: linux + GOARCH: ${{ matrix.arch }} + CGO_ENABLED: "0" + run: go build -trimpath -o "${RUNNER_TEMP}/finalweave-node-${GOARCH}" ./cmd/finalweave-node + + go-ci: + name: Go CI + if: always() + needs: + - go-quality + - go-unit + - go-race + - go-build + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + steps: + - name: Require every Go gate + shell: bash + env: + QUALITY_RESULT: ${{ needs['go-quality'].result }} + UNIT_RESULT: ${{ needs['go-unit'].result }} + RACE_RESULT: ${{ needs['go-race'].result }} + BUILD_RESULT: ${{ needs['go-build'].result }} + run: | + for result in "$QUALITY_RESULT" "$UNIT_RESULT" "$RACE_RESULT" "$BUILD_RESULT"; do + if [ "$result" != "success" ]; then + echo "::error::one or more Go gates did not succeed" + exit 1 + fi + done diff --git a/.gitignore b/.gitignore index e203820..0286dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __pycache__/ *.py[cod] *.log *.tmp +/bin/ +/dist/ +coverage*.out diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 091e622..abc2c8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,8 @@ PR 描述必须: 没有关联 Issue 的紧急修复必须解释原因,并在合并后补齐记录。 +由 GitHub Dependabot App 自动创建的依赖和安全更新 PR 不要求另建 Issue,也不要求人工改写其生成的分支名或正文;Dependabot alert、advisory 和生成的变更记录承担追踪作用。这类 PR 仍必须通过全部 required checks、依赖审查和项目规定的审批/合并规则,人工追加的非依赖改动必须另开 Issue 和 PR。 + ### 提交格式 ```text @@ -118,13 +120,20 @@ git config commit.template .github/commit_message_template.txt ### 验证门 -当前仓库处于规范阶段,所有改动至少运行: +当前仓库处于规范与代码 Bootstrap 阶段,所有改动至少运行: ```bash git diff --check python3 scripts/check_docs.py +go mod verify +go vet ./... +go test -count=1 -mod=readonly ./... +go test -race -count=1 -mod=readonly ./... +python3 scripts/check_go_architecture.py ``` +修改 Go module 后还必须运行 `go mod tidy` 并确认 `go.mod`、`go.sum` 没有非预期差异。涉及尚未落地的协议、存储、网络、执行或证明能力时,应按对应风险增加 property、Fuzz、向量、模型、Byzantine、网络分区、崩溃恢复、Chaos 或性能门禁;当前 Bootstrap 不能替代这些检查。 + 文档或 ADR 变更还必须检查: - 所有相对链接和文档入口有效。 @@ -180,6 +189,8 @@ The `dependabot/` namespace is reserved for the GitHub Dependabot App. - PR bodies link the issue, summarize impact, cover all relevant safety and compatibility boundaries, list validation, and describe risk and rollback. - Security vulnerabilities use the private process in [SECURITY.md](SECURITY.md), not public issues. +Dependency and security-update PRs authored by the GitHub Dependabot App are exempt from a separate issue and from manually rewriting the generated branch or body. The alert, advisory, and generated update record provide traceability. These PRs still require every required check, dependency review, and normal approval/merge control; unrelated human-authored changes require their own issue and PR. + ### Safety review Review every relevant change against quorum and epoch rules, BatchAC meaning, deterministic ordering, serial-equivalent execution, exact finality-proof binding, canonical encoding, durable recovery, bounded Byzantine work, compatibility, and rollback. @@ -188,11 +199,16 @@ No optimization may trade away safety, determinism, verifiability, or recoverabi ### Validation -The current specification repository requires at least: +The repository is now in the specification and code-bootstrap phase. Every change requires at least: ```bash git diff --check python3 scripts/check_docs.py +go mod verify +go vet ./... +go test -count=1 -mod=readonly ./... +go test -race -count=1 -mod=readonly ./... +python3 scripts/check_go_architecture.py ``` -As implementation code lands, each PR must add and run the applicable unit, race, property, fuzz, cross-implementation vector, model, Byzantine, partition, crash-recovery, snapshot, chaos, and performance gates. Document every skipped relevant check and its residual risk. +After changing module dependencies, run `go mod tidy` and review every `go.mod` and `go.sum` change. Each PR must add and run the applicable property, fuzz, cross-implementation vector, model, Byzantine, partition, crash-recovery, snapshot, chaos, and performance gates as implementation code lands. Document every skipped relevant check and its residual risk; the bootstrap gates do not replace feature-specific validation. diff --git a/README.md b/README.md index cabccdc..6b17139 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # FinalWeave -[![Docs CI](https://github.com/wowtrust/final-weave/actions/workflows/ci.yml/badge.svg)](https://github.com/wowtrust/final-weave/actions/workflows/ci.yml) +[![CI](https://github.com/wowtrust/final-weave/actions/workflows/ci.yml/badge.svg)](https://github.com/wowtrust/final-weave/actions/workflows/ci.yml) [![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPL--3.0--only-blue.svg)](LICENSE) -[文档中心](doc/README.md) · [学习路线](doc/tutorial/00-learning-path.md) · [系统架构](doc/01-system-architecture.md) · [协议规范](doc/protocol/README.md) · [工程规范](doc/engineering/README.md) · [贡献指南](CONTRIBUTING.md) +[文档中心](doc/README.md) · [学习路线](doc/tutorial/00-learning-path.md) · [系统架构](doc/01-system-architecture.md) · [代码架构](doc/07-code-architecture.md) · [协议规范](doc/protocol/README.md) · [工程规范](doc/engineering/README.md) · [贡献指南](CONTRIBUTING.md) > **FinalWeave — Parallel by design, final by proof.** > @@ -12,7 +12,7 @@ FinalWeave 是一套面向多组织协作场景的许可型、多账本、确定性最终性 BlockDAG 区块链设计。它把并行数据可用性、直接 DAG 排序、确定性并行执行和可独立验证的最终性证明组合成一条完整链路。 > [!IMPORTANT] -> FinalWeave 当前处于**架构设计与协议规范阶段**。本仓库尚不包含可运行节点、CLI、SDK、容器镜像或正式版本,也没有 FinalWeave 自身的生产 TPS、延迟或稳定性数据。文档中的命令、目录和接口属于目标设计,不能视为已经交付的能力。 +> FinalWeave 当前处于**架构设计、协议规范与代码 Bootstrap 阶段**。仓库已经包含 Go module、`finalweave-node version` 诊断命令、构建信息和 v1 quorum 参数校验,但尚无可运行的共识节点、业务 CLI、SDK、API、容器镜像或正式版本,也没有 FinalWeave 自身的生产 TPS、延迟或稳定性数据。除明确标记为已实现的 Bootstrap 能力外,文档中的命令、目录和接口均属于目标设计,不能视为已经交付。 ## FinalWeave 解决什么问题 @@ -110,19 +110,24 @@ FinalWeave 面向高持续写入、多机构 Byzantine 信任边界、可恢复 完整目录、统一术语、文档优先级和推荐路线见[文档中心](doc/README.md)。 -## 本地阅读与校验 +## 本地构建与校验 ```bash git clone https://github.com/wowtrust/final-weave.git cd final-weave +go mod download +go test ./... +go run ./cmd/finalweave-node version +go run ./cmd/finalweave-node version --output json python3 scripts/check_docs.py +python3 scripts/check_go_architecture.py ``` -已配置 GitHub SSH key 的开发者也可以使用 `git@github.com:wowtrust/final-weave.git`。文档采用普通 Markdown 和内嵌 Mermaid,不需要专用站点生成器;校验脚本仅依赖 Python 3 标准库。 +当前二进制只提供可复用、可测试的版本诊断入口,不会启动网络、监听端口或运行共识。完整本地门禁还包括 `go vet ./...` 和 `go test -race ./...`。已配置 GitHub SSH key 的开发者也可以使用 `git@github.com:wowtrust/final-weave.git`。文档采用普通 Markdown 和内嵌 Mermaid,不需要专用站点生成器;两个校验脚本仅依赖 Python 3 标准库,其中代码架构检查会调用 Go 工具链。 ## 当前路线 -当前仓库交付的是阶段性设计与协议规范文档,其中既有 Accepted ADR 和规范性协议,也有仍需实现与验证的工程设计。实施按依赖关系推进: +当前仓库交付阶段性设计与协议规范文档,以及一套最小 Go Bootstrap。Bootstrap 不是实施路线的阶段 0 完成声明;确定性模拟器、稳定错误模型、日志与指标、SBOM、跨实现向量等门禁仍需按独立 Issue 落地。后续实现按依赖关系推进: ```text schema 与测试向量 diff --git a/cmd/finalweave-node/main.go b/cmd/finalweave-node/main.go new file mode 100644 index 0000000..17ca9e2 --- /dev/null +++ b/cmd/finalweave-node/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "os" + + "github.com/wowtrust/final-weave/internal/buildinfo" + "github.com/wowtrust/final-weave/internal/cli" +) + +func main() { + if err := cli.NewNodeCommand(os.Stdout, os.Stderr, buildinfo.Current()).Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/doc/07-code-architecture.md b/doc/07-code-architecture.md new file mode 100644 index 0000000..4cb1527 --- /dev/null +++ b/doc/07-code-architecture.md @@ -0,0 +1,109 @@ +# FinalWeave 代码架构与 Bootstrap 基线 + +> 状态:Bootstrap 已实现;共识节点、网络、存储、执行、证明和 SDK 尚未实现 +> 权威目标结构:[实施路线](04-implementation-roadmap.md) +> 开发教程:[开发环境、目标代码架构与 Bootstrap](tutorial/03-development-environment-and-codebase.md) + +本文只描述仓库中已经存在的代码,以及后续实现必须遵守的工程边界。协议对象、算法和接口仍以 Accepted ADR 与 `doc/protocol/` 为权威;本文件不新增共识语义。 + +## 1. 当前交付范围 + +当前代码提供四项真实能力: + +1. `github.com/wowtrust/final-weave` 单 Go module,最低 Go 版本为 1.26.5,CI 精确使用 1.26.5; +2. `finalweave-node version` 文本/JSON 诊断输出; +3. `internal/buildinfo` 的版本、提交、构建时间和 Go runtime 元数据; +4. `pkg/types` 中严格的 v1 `n=3f+1`、`q=2f+1`、`k=f+1` 参数推导。 + +裸执行 `finalweave-node` 会以非零状态明确拒绝启动,且不存在 `run`、`start` 或 `serve` 子命令;`--help` 和 `version` 是仅有的成功诊断路径。因此它不会打开存储、监听网络、产生签名、处理交易或声称 validator ready。此限制是防止 Bootstrap 被误当成共识实现的安全边界。 + +## 2. 通用技术选型 + +工程基线复用 TrustDB 已验证的通用选择,但不复制它的业务代码或完整依赖图: + +| 项目 | 当前选择 | 边界 | +| --- | --- | --- | +| Go module | 单 module;`go.mod` 最低 1.26.5,CI 固定 1.26.5 | 本地更新工具链允许先验证,但合并结果以固定 CI 为准;暂不使用 `go.work` 或多 module | +| CLI | Cobra 1.10.2 | `cmd` 只负责装配和退出;诊断命令不触发未来节点配置 | +| 核心包 | 优先标准库 | 没有真实消费者前不引入数据库、网络、日志或配置依赖 | +| 测试 | unit、race、Fuzz target | Fuzz seed 会随普通测试执行;长时间 Fuzz 后续单独加门禁 | +| CI | module verify/tidy、gofmt、vet、unit、race、架构检查、Linux 双架构构建 | 不创建空 integration、E2E、Chaos 或 benchmark job | +| 依赖更新 | Dependabot security updates | routine Go 版本 PR 默认关闭,避免淹没安全修复 | + +Viper、zerolog、Prometheus、CBOR、gRPC、Pebble 等仍是后续真实适配器可评估的通用选型,不是当前依赖。FinalWeave 的本地配置、规范编码和严格签名验证比 TrustDB 具有额外 fail-closed 要求,必须在相应规范、负向语料和测试向量就绪后独立实现。 + +## 3. 当前目录与职责 + +```text +cmd/finalweave-node/ 薄进程入口;只装配诊断命令 +internal/buildinfo/ 进程构建元数据,不进入协议身份或哈希 +internal/cli/ CLI 命令装配和输出格式 +pkg/types/ 可复用的纯协议值与不变量 +scripts/ 文档与 Go 依赖边界检查 +``` + +没有真实实现的 `api/`、`storage/`、`network/`、`execution/`、`consensus/`、`testkit/` 和其他二进制不会以空目录或 noop package 预先创建。新增目录必须随第一个真实消费者、失败语义和测试一起进入仓库。 + +## 4. 依赖方向 + +```mermaid +flowchart TB + CMD["cmd/finalweave-node"] --> CLI["internal/cli"] + CLI --> BUILD["internal/buildinfo"] + TYPES["pkg/types"] +``` + +当前强制规则: + +- `pkg/types` 是只使用标准库的依赖叶子,不导入本 module 或第三方包; +- `internal/buildinfo` 只依赖 Go 标准库; +- `pkg/` 不得导入 `internal/`,避免对外可复用协议包依赖进程实现; +- 生产包不得依赖未来的 `testkit`; +- API DTO、Protobuf、数据库记录、日志对象和本地配置不得成为共识对象或哈希输入; +- `scripts/check_go_architecture.py` 在 CI 中检查上述可静态验证的边界。 + +后续目标依赖仍遵循:适配器和进程装配依赖应用服务,应用服务依赖窄 core ports,协议与执行核心依赖 `types/codec/crypto`;依赖方向不得反转。 + +## 5. 生命周期与副作用原则 + +未来组件应遵守以下通用规则: + +- 接口由使用方定义,只包含真实消费者需要的方法; +- 构造函数完成校验和装配,不启动 goroutine; +- 每个 Ledger runtime 拥有一个根 context,每个 goroutine 有唯一 owner 和可等待退出路径; +- channel、队列、缓存、分页、重试和并发度必须有硬上限及满载策略; +- 共识、执行和证明核心不直接读取墙钟、环境变量、随机数、网络或全局 logger; +- network、storage、API 和 signer 都通过窄接口注入; +- 恢复和交叉校验完成前不得开放 readiness 或外部 listener; +- Safety WAL 在任何可能双签的签名之前严格持久化,恢复歧义必须 fail closed。 + +这些原则与 TrustDB 的有界工作、耐久边界、恢复先于服务和显式装配一致;FinalWeave 不继承 TrustDB 特定的证据层级、批处理、锚定、存储 schema 或 group-fsync 语义。 + +## 6. 协议实现门禁 + +当前 quorum 工具只编码文档已经一致冻结的数值不变量。它不验证 ValidatorID、密钥唯一性、epoch、签名、证书或数据可用性,也不实现 FinalDAG-C。 + +以下能力不得仅凭 prose 或临时示例发布稳定 API: + +- canonical CBOR 与对象 ID:先提交机器可读 schema、独立 golden vectors 和非规范输入语料; +- Ed25519 验证:先满足 canonical encoding、subgroup/torsion 等严格负向 corpus; +- SafetySigner 与 WAL:先冻结结构化 intent、fsync、重放和 `SAFETY_HALT` 契约; +- 节点配置:本地配置与 Genesis/链上协议配置严格分离,拒绝未知字段、重复 key、隐式类型和无单位时长; +- 共识、执行和证明:禁止 noop、单节点成功路径或只返回固定 `FINALIZED` 的占位实现。 + +## 7. 验证与构建 + +```bash +go mod download +go mod verify +go vet ./... +go test -count=1 -mod=readonly ./... +go test -race -count=1 -mod=readonly ./... +python3 scripts/check_go_architecture.py +python3 scripts/check_docs.py +go build -trimpath -o ./bin/finalweave-node ./cmd/finalweave-node +``` + +未来发布构建必须通过 `-ldflags -X` 注入 `internal/buildinfo` 的 version、commit 和 date;当前尚无发布流水线。无论是否注入,这些值都只用于诊断,不参与协议版本、对象身份或共识判断。 + +这套 Bootstrap 只是可持续实现的起点,不代表[实施路线](04-implementation-roadmap.md)阶段 0 已完成。阶段 0 仍需要独立落地错误码、严格配置、日志脱敏、metrics、虚拟时钟、确定性网络模拟器、SBOM、依赖策略和相应 ADR。 diff --git a/doc/README.md b/doc/README.md index c9a4ac1..0a2c3c3 100644 --- a/doc/README.md +++ b/doc/README.md @@ -62,6 +62,7 @@ FinalWeave 的优化优先级是:**正确功能与安全边界 > 可持续性 | [实施路线](04-implementation-roadmap.md) | 从 schema、BatchAC、DAG 到并行执行和生产化的依赖顺序 | | [配置规范](05-configuration-reference.md) | 本地、Genesis、链上配置及静态/动态边界 | | [成熟方案比较与取舍](06-comparison-and-tradeoffs.md) | 与线性 BFT、成熟联盟链、直接 DAG 和并行执行路线比较 | +| [代码架构与 Bootstrap 基线](07-code-architecture.md) | 已实现代码、通用技术选型、依赖方向与后续扩展门禁 | | [技术参考](references.md) | 论文、RFC、证据口径与建议阅读顺序 | ### 4.2 核心协议 diff --git a/doc/tutorial/03-development-environment-and-codebase.md b/doc/tutorial/03-development-environment-and-codebase.md index 2f67eaf..a5bc61c 100644 --- a/doc/tutorial/03-development-environment-and-codebase.md +++ b/doc/tutorial/03-development-environment-and-codebase.md @@ -5,6 +5,9 @@ > 重要说明:本文中的目标目录、包和 CLI 是设计;在对应阶段合并前,不应宣称命令或文件已经存在 > 上一篇:[02-finalweave-transaction-lifecycle.md](02-finalweave-transaction-lifecycle.md) | 下一篇:[04-first-contribution-tutorial.md](04-first-contribution-tutorial.md) +> [!NOTE] +> 当前仓库已经实现单 Go module、`finalweave-node version`、`internal/buildinfo`、v1 `n/f/q/k` 参数校验和基础 Go CI。它们是 Bootstrap,不是可运行共识节点,也不表示下方目标目录已经全部存在。当前代码边界见[代码架构与 Bootstrap 基线](../07-code-architecture.md)。 + 上一章已经把 Alice 的 KVPut 追踪到最终证明。现在的问题变成:怎样组织 Go 工程,才能让 SDK 的规范字节、BatchAC、FinalDAG-C、安全签名槽、并行执行根和证明验证各守边界,同时还能端到端承载同一笔交易?本章从零给出工程 Bootstrap 顺序。 ## 1. 三类命令标签 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..00f9e3f --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/wowtrust/final-weave + +go 1.26.5 + +require github.com/spf13/cobra v1.10.2 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6ee3e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go new file mode 100644 index 0000000..fbba82f --- /dev/null +++ b/internal/buildinfo/buildinfo.go @@ -0,0 +1,34 @@ +// Package buildinfo exposes immutable metadata about the current binary. +// Release builds inject version, commit, and date with -ldflags -X. +package buildinfo + +import "runtime" + +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +// Info describes one FinalWeave binary build. These fields are diagnostic +// metadata and are not part of any consensus transcript or protocol identity. +type Info struct { + Version string `json:"version"` + Commit string `json:"commit"` + Date string `json:"date"` + GoVersion string `json:"go"` + OS string `json:"os"` + Arch string `json:"arch"` +} + +// Current returns build metadata for the running binary. +func Current() Info { + return Info{ + Version: version, + Commit: commit, + Date: date, + GoVersion: runtime.Version(), + OS: runtime.GOOS, + Arch: runtime.GOARCH, + } +} diff --git a/internal/buildinfo/buildinfo_test.go b/internal/buildinfo/buildinfo_test.go new file mode 100644 index 0000000..06e9ad5 --- /dev/null +++ b/internal/buildinfo/buildinfo_test.go @@ -0,0 +1,13 @@ +package buildinfo + +import "testing" + +func TestCurrentIncludesRuntimeMetadata(t *testing.T) { + info := Current() + if info.Version == "" || info.Commit == "" || info.Date == "" { + t.Fatalf("build metadata contains an empty field: %+v", info) + } + if info.GoVersion == "" || info.OS == "" || info.Arch == "" { + t.Fatalf("runtime metadata contains an empty field: %+v", info) + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..cd58290 --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,80 @@ +// Package cli assembles FinalWeave command-line interfaces. +// +// The bootstrap intentionally exposes diagnostics only. A node run command +// will be added with the first real runtime; this package must never present a +// placeholder process as a functioning validator. +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/wowtrust/final-weave/internal/buildinfo" +) + +const ( + outputText = "text" + outputJSON = "json" +) + +// ErrNodeRuntimeUnavailable is returned when the bootstrap binary is invoked +// as a node. Only diagnostic subcommands are available until a real runtime is +// implemented. +var ErrNodeRuntimeUnavailable = errors.New("node runtime is not implemented; use 'finalweave-node version' or '--help'") + +// NewNodeCommand returns the root command for finalweave-node. +func NewNodeCommand(out, errOut io.Writer, info buildinfo.Info) *cobra.Command { + root := &cobra.Command{ + Use: "finalweave-node", + Short: "FinalWeave node bootstrap diagnostics", + Long: "FinalWeave node bootstrap diagnostics. The consensus runtime is not implemented yet.", + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.NoArgs, + RunE: func(*cobra.Command, []string) error { + return ErrNodeRuntimeUnavailable + }, + } + root.SetOut(out) + root.SetErr(errOut) + root.CompletionOptions.DisableDefaultCmd = true + root.AddCommand(newVersionCommand(info)) + return root +} + +func newVersionCommand(info buildinfo.Info) *cobra.Command { + output := outputText + cmd := &cobra.Command{ + Use: "version", + Short: "Show build version information", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + switch output { + case outputText: + _, err := fmt.Fprintf( + cmd.OutOrStdout(), + "finalweave-node %s\ncommit: %s\nbuilt: %s\ngo: %s\nplatform: %s/%s\n", + info.Version, + info.Commit, + info.Date, + info.GoVersion, + info.OS, + info.Arch, + ) + return err + case outputJSON: + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetEscapeHTML(false) + return encoder.Encode(info) + default: + return fmt.Errorf("unsupported output format %q: use text or json", output) + } + }, + } + cmd.Flags().StringVarP(&output, "output", "o", outputText, "output format: text or json") + return cmd +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..6511e97 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,83 @@ +package cli + +import ( + "bytes" + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/wowtrust/final-weave/internal/buildinfo" +) + +var testBuildInfo = buildinfo.Info{ + Version: "v0.0.0-test", + Commit: "0123456789abcdef", + Date: "2026-07-23T00:00:00Z", + GoVersion: "go1.test", + OS: "testos", + Arch: "testarch", +} + +func TestVersionText(t *testing.T) { + var stdout bytes.Buffer + cmd := NewNodeCommand(&stdout, &bytes.Buffer{}, testBuildInfo) + cmd.SetArgs([]string{"version"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + for _, want := range []string{ + "finalweave-node v0.0.0-test", + "commit: 0123456789abcdef", + "built: 2026-07-23T00:00:00Z", + "go: go1.test", + "platform: testos/testarch", + } { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("text output %q does not contain %q", stdout.String(), want) + } + } +} + +func TestVersionJSON(t *testing.T) { + var stdout bytes.Buffer + cmd := NewNodeCommand(&stdout, &bytes.Buffer{}, testBuildInfo) + cmd.SetArgs([]string{"version", "--output", "json"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + var got buildinfo.Info + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("json.Unmarshal() error = %v; output = %q", err, stdout.String()) + } + if got != testBuildInfo { + t.Fatalf("JSON build info = %+v, want %+v", got, testBuildInfo) + } +} + +func TestVersionRejectsUnsupportedOutput(t *testing.T) { + cmd := NewNodeCommand(&bytes.Buffer{}, &bytes.Buffer{}, testBuildInfo) + cmd.SetArgs([]string{"version", "--output", "yaml"}) + + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), "unsupported output format") { + t.Fatalf("Execute() error = %v, want unsupported output format", err) + } +} + +func TestRootFailsClosedWithoutDiagnosticSubcommand(t *testing.T) { + cmd := NewNodeCommand(&bytes.Buffer{}, &bytes.Buffer{}, testBuildInfo) + if !cmd.CompletionOptions.DisableDefaultCmd { + t.Fatal("bootstrap must not expose Cobra's implicit completion command") + } + if got := cmd.Commands(); len(got) != 1 || got[0].Name() != "version" { + t.Fatalf("bootstrap subcommands = %v, want version only", got) + } + if err := cmd.Execute(); !errors.Is(err, ErrNodeRuntimeUnavailable) { + t.Fatalf("bare Execute() error = %v, want ErrNodeRuntimeUnavailable", err) + } +} diff --git a/pkg/types/quorum.go b/pkg/types/quorum.go new file mode 100644 index 0000000..4ba257d --- /dev/null +++ b/pkg/types/quorum.go @@ -0,0 +1,109 @@ +// Package types contains small, reusable protocol value types and invariants. +// It must not depend on process configuration, transports, databases, or APIs. +package types + +import ( + "errors" + "fmt" +) + +const ( + // MinValidatorCount is the smallest FinalWeave v1 validator set. + MinValidatorCount = 4 + // MaxValidatorCount is the largest set supported by the v1 GF(2^8) + // availability profile. + MaxValidatorCount = 253 +) + +var ( + // ErrValidatorCountOutOfRange reports a validator count outside the v1 + // absolute bounds. + ErrValidatorCountOutOfRange = errors.New("validator count is outside FinalWeave v1 bounds") + // ErrValidatorCountNotThreeFPlusOne reports a validator count that cannot + // be expressed exactly as n = 3f + 1. + ErrValidatorCountNotThreeFPlusOne = errors.New("validator count is not exactly 3f+1") +) + +// QuorumParameters is a validated, immutable FinalWeave v1 n/f/q/k tuple. +// Its zero value is invalid. Every accessor panics on an invalid value instead +// of returning fail-open thresholds; construct values with +// NewQuorumParameters and always handle its error. +type QuorumParameters struct { + validatorCount int + maxByzantine int + quorum int + recoveryThreshold int +} + +// NewQuorumParameters derives the v1 thresholds for an exact validator count. +// It accepts int so callers can pass len(validators) without narrowing first. +func NewQuorumParameters(validatorCount int) (QuorumParameters, error) { + if validatorCount < MinValidatorCount || validatorCount > MaxValidatorCount { + return QuorumParameters{}, fmt.Errorf( + "%w: got %d, want %d..%d", + ErrValidatorCountOutOfRange, + validatorCount, + MinValidatorCount, + MaxValidatorCount, + ) + } + if (validatorCount-1)%3 != 0 { + return QuorumParameters{}, fmt.Errorf( + "%w: got %d", + ErrValidatorCountNotThreeFPlusOne, + validatorCount, + ) + } + + maxByzantine := (validatorCount - 1) / 3 + return QuorumParameters{ + validatorCount: validatorCount, + maxByzantine: maxByzantine, + quorum: 2*maxByzantine + 1, + recoveryThreshold: maxByzantine + 1, + }, nil +} + +// ValidatorCount returns n. +func (p QuorumParameters) ValidatorCount() int { + p.mustBeValid() + return p.validatorCount +} + +// MaxByzantine returns f. +func (p QuorumParameters) MaxByzantine() int { + p.mustBeValid() + return p.maxByzantine +} + +// Quorum returns q, the number of distinct valid members required by a v1 +// quorum certificate. +func (p QuorumParameters) Quorum() int { + p.mustBeValid() + return p.quorum +} + +// RecoveryThreshold returns k, the number of valid fragments required to +// reconstruct a v1 batch body. +func (p QuorumParameters) RecoveryThreshold() int { + p.mustBeValid() + return p.recoveryThreshold +} + +// MinimumQuorumIntersection returns the lower bound for the intersection of +// any two quorums in the same validator set. +func (p QuorumParameters) MinimumQuorumIntersection() int { + p.mustBeValid() + return 2*p.quorum - p.validatorCount +} + +func (p QuorumParameters) mustBeValid() { + if p.validatorCount < MinValidatorCount || + p.validatorCount > MaxValidatorCount || + (p.validatorCount-1)%3 != 0 || + p.maxByzantine != (p.validatorCount-1)/3 || + p.quorum != 2*p.maxByzantine+1 || + p.recoveryThreshold != p.maxByzantine+1 { + panic("types: invalid QuorumParameters; use NewQuorumParameters and handle its error") + } +} diff --git a/pkg/types/quorum_test.go b/pkg/types/quorum_test.go new file mode 100644 index 0000000..f7c5c8c --- /dev/null +++ b/pkg/types/quorum_test.go @@ -0,0 +1,146 @@ +package types + +import ( + "errors" + "testing" +) + +func TestNewQuorumParameters(t *testing.T) { + tests := []struct { + name string + n int + f int + q int + k int + }{ + {name: "minimum", n: 4, f: 1, q: 3, k: 2}, + {name: "seven", n: 7, f: 2, q: 5, k: 3}, + {name: "ten", n: 10, f: 3, q: 7, k: 4}, + {name: "maximum", n: 253, f: 84, q: 169, k: 85}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewQuorumParameters(tt.n) + if err != nil { + t.Fatalf("NewQuorumParameters(%d) error = %v", tt.n, err) + } + if got.ValidatorCount() != tt.n || got.MaxByzantine() != tt.f || + got.Quorum() != tt.q || got.RecoveryThreshold() != tt.k { + t.Fatalf( + "NewQuorumParameters(%d) = n/f/q/k %d/%d/%d/%d, want %d/%d/%d/%d", + tt.n, + got.ValidatorCount(), + got.MaxByzantine(), + got.Quorum(), + got.RecoveryThreshold(), + tt.n, + tt.f, + tt.q, + tt.k, + ) + } + if got.MinimumQuorumIntersection() != tt.k { + t.Fatalf("minimum quorum intersection = %d, want %d", got.MinimumQuorumIntersection(), tt.k) + } + }) + } +} + +func TestNewQuorumParametersRejectsInvalidCounts(t *testing.T) { + tests := []struct { + n int + wantErr error + }{ + {n: -1, wantErr: ErrValidatorCountOutOfRange}, + {n: 0, wantErr: ErrValidatorCountOutOfRange}, + {n: 3, wantErr: ErrValidatorCountOutOfRange}, + {n: 5, wantErr: ErrValidatorCountNotThreeFPlusOne}, + {n: 252, wantErr: ErrValidatorCountNotThreeFPlusOne}, + {n: 254, wantErr: ErrValidatorCountOutOfRange}, + {n: 65_540, wantErr: ErrValidatorCountOutOfRange}, + {n: int(^uint(0) >> 1), wantErr: ErrValidatorCountOutOfRange}, + } + + for _, tt := range tests { + _, err := NewQuorumParameters(tt.n) + if !errors.Is(err, tt.wantErr) { + t.Errorf("NewQuorumParameters(%d) error = %v, want errors.Is(%v)", tt.n, err, tt.wantErr) + } + } +} + +func TestQuorumParametersExhaustiveV1Domain(t *testing.T) { + for n := -1; n <= MaxValidatorCount+1; n++ { + params, err := NewQuorumParameters(n) + valid := n >= MinValidatorCount && n <= MaxValidatorCount && (n-1)%3 == 0 + if valid != (err == nil) { + t.Fatalf("n=%d validity mismatch: error=%v", n, err) + } + if !valid { + continue + } + if params.ValidatorCount() != 3*params.MaxByzantine()+1 { + t.Fatalf("n=%d does not satisfy n=3f+1", n) + } + if params.Quorum() != 2*params.MaxByzantine()+1 { + t.Fatalf("n=%d does not satisfy q=2f+1", n) + } + if params.RecoveryThreshold() != params.MaxByzantine()+1 { + t.Fatalf("n=%d does not satisfy k=f+1", n) + } + if params.MinimumQuorumIntersection() != params.RecoveryThreshold() { + t.Fatalf("n=%d quorum intersection is below k", n) + } + } +} + +func FuzzNewQuorumParameters(f *testing.F) { + for _, n := range []int{-1, 0, 3, 4, 5, 7, 10, 253, 254, 65_540, int(^uint(0) >> 1)} { + f.Add(n) + } + + f.Fuzz(func(t *testing.T, n int) { + params, err := NewQuorumParameters(n) + valid := n >= MinValidatorCount && n <= MaxValidatorCount && (n-1)%3 == 0 + if !valid { + if err == nil { + t.Fatalf("NewQuorumParameters(%d) succeeded for an invalid v1 count", n) + } + return + } + if err != nil { + t.Fatalf("NewQuorumParameters(%d) error = %v", n, err) + } + if params.ValidatorCount() != 3*params.MaxByzantine()+1 || + params.Quorum() != 2*params.MaxByzantine()+1 || + params.RecoveryThreshold() != params.MaxByzantine()+1 { + t.Fatalf("NewQuorumParameters(%d) returned inconsistent parameters", n) + } + }) +} + +func TestQuorumParametersZeroValueFailsClosed(t *testing.T) { + var zero QuorumParameters + accessors := []struct { + name string + call func() int + }{ + {name: "validator count", call: zero.ValidatorCount}, + {name: "maximum Byzantine", call: zero.MaxByzantine}, + {name: "quorum", call: zero.Quorum}, + {name: "recovery threshold", call: zero.RecoveryThreshold}, + {name: "minimum quorum intersection", call: zero.MinimumQuorumIntersection}, + } + + for _, accessor := range accessors { + t.Run(accessor.name, func(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("invalid zero value accessor did not panic") + } + }() + _ = accessor.call() + }) + } +} diff --git a/scripts/check_go_architecture.py b/scripts/check_go_architecture.py new file mode 100644 index 0000000..9662982 --- /dev/null +++ b/scripts/check_go_architecture.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Enforce the initial FinalWeave Go dependency boundaries.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +MODULE = "github.com/wowtrust/final-weave" +ROOT = Path(__file__).resolve().parents[1] +PUBLIC_ROOT = MODULE + "/pkg" +INTERNAL_ROOT = MODULE + "/internal" +TESTKIT_ROOT = PUBLIC_ROOT + "/testkit" + + +def in_package_tree(package: str, root: str) -> bool: + return package == root or package.startswith(root + "/") + + +def standard_packages() -> set[str]: + result = subprocess.run( + ["go", "list", "std"], + check=True, + capture_output=True, + cwd=ROOT, + text=True, + ) + return set(result.stdout.splitlines()) + + +def go_packages() -> list[tuple[str, list[str]]]: + template = "{{.ImportPath}}\t{{join .Imports \" \"}}" + result = subprocess.run( + ["go", "list", "-mod=readonly", "-f", template, "./..."], + check=True, + capture_output=True, + cwd=ROOT, + text=True, + ) + packages: list[tuple[str, list[str]]] = [] + for line in result.stdout.splitlines(): + path, imports = (line.split("\t", 1) + [""])[:2] + packages.append((path, imports.split())) + return packages + + +def main() -> int: + errors: list[str] = [] + standard = standard_packages() + + for package, imports in go_packages(): + module_imports = [item for item in imports if in_package_tree(item, MODULE)] + non_standard_imports = [item for item in imports if item not in standard] + + if in_package_tree(package, PUBLIC_ROOT): + forbidden = [item for item in module_imports if in_package_tree(item, INTERNAL_ROOT)] + for item in forbidden: + errors.append(f"{package} must not import internal package {item}") + + if package == MODULE + "/pkg/types" and non_standard_imports: + errors.append( + f"{package} must remain a standard-library-only dependency leaf; " + f"found {', '.join(non_standard_imports)}" + ) + + if package == MODULE + "/internal/buildinfo" and non_standard_imports: + errors.append( + f"{package} must use only the standard library; " + f"found {', '.join(non_standard_imports)}" + ) + + if not in_package_tree(package, TESTKIT_ROOT): + for item in imports: + if in_package_tree(item, TESTKIT_ROOT): + errors.append(f"{package} must not depend on testkit package {item}") + + if errors: + print("Go architecture check failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + print("Go architecture check passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())