Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
12 changed files with 164 additions and 226 deletions
|
|
@ -1,40 +0,0 @@
|
||||||
# .forgejo/workflows/gitleaks.yml
|
|
||||||
#
|
|
||||||
# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at
|
|
||||||
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
|
|
||||||
# (task #295).
|
|
||||||
#
|
|
||||||
# Pairs with the pre-receive hook installed on every bare repo — that one is
|
|
||||||
# the strict enforcement layer (rejects the push); this one provides the
|
|
||||||
# per-PR red ✗ that branch-protection rules can require before merge.
|
|
||||||
#
|
|
||||||
# Layer 1 (this workflow): visible per-PR status, can be a required check.
|
|
||||||
# Layer 2 (pre-receive hook): strict enforcement at the server.
|
|
||||||
# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos.
|
|
||||||
|
|
||||||
name: gitleaks
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
scan:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
# Full history — gitleaks needs depth to scan a commit range.
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: install gitleaks
|
|
||||||
run: |
|
|
||||||
curl -sSL -o gl.tar.gz \
|
|
||||||
https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
|
|
||||||
tar xzf gl.tar.gz gitleaks
|
|
||||||
chmod +x gitleaks
|
|
||||||
./gitleaks version
|
|
||||||
|
|
||||||
- name: scan
|
|
||||||
run: |
|
|
||||||
./gitleaks detect --source . --no-banner --redact --verbose
|
|
||||||
100
README.md
100
README.md
|
|
@ -2,38 +2,46 @@
|
||||||
|
|
||||||
Pure-Go client for the Cardano [Mithril](https://mithril.network) protocol.
|
Pure-Go client for the Cardano [Mithril](https://mithril.network) protocol.
|
||||||
|
|
||||||
Mithril is Cardano's stake-based certified-snapshot system. A new node
|
Mithril is Cardano's stake-based certified-snapshot system — it lets a new
|
||||||
bootstraps the chain from a cryptographically-verified snapshot instead
|
node bootstrap the chain from a cryptographically-verified snapshot
|
||||||
of replaying every block from genesis.
|
instead of replaying every block from genesis.
|
||||||
|
|
||||||
The reference [`mithril-client`](https://github.com/input-output-hk/mithril)
|
The official [`mithril-client`](https://github.com/input-output-hk/mithril)
|
||||||
is Rust. `mithril-go` is a pure-Go reimplementation: single static binary,
|
is Rust. `mithril-go` is a pure-Go reimplementation that produces a single
|
||||||
no CGo, no `blst`, no upstream Rust runtime. Also ships an MCP stdio
|
static binary with no CGo, no upstream Rust runtime, and an MCP-native
|
||||||
server for agent use.
|
tool surface for AI agents — useful for:
|
||||||
|
|
||||||
|
- Embedding Mithril bootstrap into Go-based Cardano tooling (alongside
|
||||||
|
`gouroboros`, `dingo`, etc.)
|
||||||
|
- Constrained ARM/embedded targets where shipping the Rust binary + its
|
||||||
|
deps is overkill
|
||||||
|
- Operators who prefer a single `go install`-able helper
|
||||||
|
- AI agents (Claude Code, Cursor, Zed, ...) that want to verify Mithril
|
||||||
|
certs as a callable tool
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Consensus-correct verification against live mainnet and preprod.
|
**Working consensus-correct verification against live mainnet and preprod.**
|
||||||
|
|
||||||
| Capability | Status |
|
| Capability | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Aggregator REST client (list, show, cert, walk-chain) | done |
|
| Aggregator REST client | ✅ list, show, cert, walk-chain |
|
||||||
| Resumable HTTP download (SHA hook + progress) | done |
|
| Resumable HTTP download (SHA hook + progress) | ✅ |
|
||||||
| Streamed zstd+tar extract (tar-slip defended) | done |
|
| Streamed zstd+tar extract (tar-slip defended) | ✅ |
|
||||||
| Genesis Ed25519 verification | done |
|
| Genesis Ed25519 verification | ✅ live mainnet + preprod |
|
||||||
| STM BLS12-381 aggregate verification | done |
|
| STM BLS12-381 aggregate verification | ✅ live mainnet + preprod |
|
||||||
| Lottery-win threshold (Taylor series, big.Rat) | done |
|
| Lottery-win threshold (Taylor series, big.Rat) | ✅ |
|
||||||
| Merkle batch-proof verification (Blake2b-256) | done |
|
| Merkle batch-proof verification (Blake2b-256) | ✅ |
|
||||||
| AVK chaining + epoch + hash continuity | done |
|
| AVK chaining + epoch + hash continuity | ✅ |
|
||||||
| Full chain verification (genesis → head) | done |
|
| **Full chain verification (genesis → head)** | ✅ live mainnet + preprod |
|
||||||
| Per-immutable SHA manifest verification | done |
|
| Per-immutable SHA manifest verification | ✅ |
|
||||||
| MCP stdio server (8 tools) | done |
|
| MCP stdio server (8 tools) | ✅ |
|
||||||
| Full immutables-download loop (8000+ files) | pending |
|
| Full immutables-download loop (8000+ files) | ⏳ |
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build ./cmd/mithril-go # ~9.5 MB single static binary
|
go build ./cmd/mithril-go # produces a 9.5 MB single static binary
|
||||||
|
|
||||||
mithril-go info -network mainnet
|
mithril-go info -network mainnet
|
||||||
mithril-go list -network mainnet
|
mithril-go list -network mainnet
|
||||||
|
|
@ -71,24 +79,27 @@ internal/
|
||||||
verify/ Genesis Ed25519 verification
|
verify/ Genesis Ed25519 verification
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upstream details that aren't documented
|
## What was non-obvious (so future-you doesn't have to dig)
|
||||||
|
|
||||||
|
Three things in the upstream Rust that aren't documented anywhere
|
||||||
|
prominent:
|
||||||
|
|
||||||
1. **DST is empty.** Mithril's BLS hash-to-G1 uses an empty domain
|
1. **DST is empty.** Mithril's BLS hash-to-G1 uses an empty domain
|
||||||
separation tag, not the IETF standard
|
separation tag, not the IETF standard `BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_`.
|
||||||
`BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_`. The Rust calls
|
The Rust calls `blst.verify(sig, msg, &[], &[], pk, ...)` — the
|
||||||
`blst.verify(sig, msg, &[], &[], pk, ...)` — the `&[]` is the empty DST.
|
`&[]` is the empty DST.
|
||||||
2. **Signed message is `signed_message_bytes || mt_root_bytes`**,
|
2. **The signed message is `signed_message_bytes || mt_root_bytes`**,
|
||||||
not `signed_message` alone. The Merkle commitment root is appended
|
not `signed_message` alone. The Merkle commitment root is appended
|
||||||
before BLS verify.
|
before BLS verify.
|
||||||
3. **Aggregation is MuSig-style scalar-weighted**, not plain.
|
3. **Aggregation is MuSig-style scalar-weighted, not plain.**
|
||||||
`t_i = Blake2b-128(Blake2b-128(σ_0‖…‖σ_{n-1}) ‖ be_u64(i))`,
|
`t_i = Blake2b-128(Blake2b-128(σ_0‖…‖σ_{n-1}) ‖ be_u64(i))`,
|
||||||
then `aggr_sig = Σ t_i·sig_i` and `aggr_vk = Σ t_i·vk_i`.
|
then `aggr_sig = Σ t_i·sig_i` and `aggr_vk = Σ t_i·vk_i`.
|
||||||
Plain summation does not interop with blst.
|
Plain summation does not interop with blst.
|
||||||
|
|
||||||
The `protocol_message` hash is SHA-256 over key-then-value, with keys
|
Plus the `protocol_message` hash is SHA-256 over key-then-value, with
|
||||||
ordered by **Rust enum declaration order**, not alphabetical.
|
keys ordered by **Rust enum declaration order**, not alphabetical.
|
||||||
|
|
||||||
## JSON + exit codes
|
## Machine / LLM usage
|
||||||
|
|
||||||
Every query command accepts `-json`:
|
Every query command accepts `-json`:
|
||||||
|
|
||||||
|
|
@ -108,11 +119,12 @@ Stable exit codes:
|
||||||
| 5 | signature verification failure (genesis or STM) |
|
| 5 | signature verification failure (genesis or STM) |
|
||||||
| 130 | canceled (SIGINT) |
|
| 130 | canceled (SIGINT) |
|
||||||
|
|
||||||
Existing codes won't renumber.
|
These are the contract — existing codes won't renumber.
|
||||||
|
|
||||||
## MCP server
|
### MCP server
|
||||||
|
|
||||||
`mithril-go mcp` brings up a Model Context Protocol stdio server.
|
`mithril-go mcp` brings up a Model Context Protocol stdio server.
|
||||||
|
Compatible with any MCP client (Claude Code, Cursor, Zed, custom agents).
|
||||||
|
|
||||||
Tools exposed:
|
Tools exposed:
|
||||||
|
|
||||||
|
|
@ -123,9 +135,15 @@ Tools exposed:
|
||||||
| `mithril_show_snapshot` | Detail for a snapshot (or `latest`) |
|
| `mithril_show_snapshot` | Detail for a snapshot (or `latest`) |
|
||||||
| `mithril_get_certificate` | Cert by hash (or `head`) |
|
| `mithril_get_certificate` | Cert by hash (or `head`) |
|
||||||
| `mithril_walk_cert_chain` | Walk previous_hash from head to genesis |
|
| `mithril_walk_cert_chain` | Walk previous_hash from head to genesis |
|
||||||
| `mithril_verify_certificate` | Ed25519 or STM BLS verify, dispatched by cert kind |
|
| `mithril_verify_certificate` | Ed25519 OR STM BLS verify, dispatched by cert kind |
|
||||||
| `mithril_verify_chain` | Full chain verify with per-step report |
|
| `mithril_verify_chain` | Full chain verify with per-step report |
|
||||||
| `mithril_verify_genesis` | Walk to genesis + Ed25519 verify |
|
| `mithril_verify_genesis` | Walk to genesis + Ed25519 verify (legacy single-purpose tool) |
|
||||||
|
|
||||||
|
Example agent flow:
|
||||||
|
```
|
||||||
|
agent: tools/call mithril_verify_chain {network: mainnet}
|
||||||
|
→ {verified: true, length: 89, genesis_hash: "...", steps: [...]}
|
||||||
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
@ -147,14 +165,14 @@ go test -tags live ./... # hits live preprod aggregator
|
||||||
## Verified against live networks (latest run)
|
## Verified against live networks (latest run)
|
||||||
|
|
||||||
```
|
```
|
||||||
mainnet chain 89 STM + 1 genesis ok
|
mainnet chain 89 STM + 1 genesis ✓
|
||||||
mainnet head 59 signers, 1972 wins ok
|
mainnet head 59 signers, 1972 wins ✓
|
||||||
mainnet genesis Ed25519 ok
|
mainnet genesis Ed25519 ✓
|
||||||
preprod chain 89 STM + 1 genesis ok
|
preprod chain 89 STM + 1 genesis ✓
|
||||||
preprod head 2 signers, 11 wins ok
|
preprod head 2 signers, 11 wins ✓
|
||||||
preprod genesis Ed25519 ok
|
preprod genesis Ed25519 ✓
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache-2.0. See `LICENSE`. Matches upstream Mithril.
|
Apache-2.0. See `LICENSE`. Matches the upstream Mithril project.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -18,39 +19,12 @@ func emitJSON(v any) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// emitJSONErr writes a structured error envelope to stdout in the shape
|
// emitJSONErr writes a structured error envelope. Mirrors the shape
|
||||||
// MCP / agent consumers expect:
|
// Claude/MCP-friendly consumers want: {"error": {"code":..., "message":...}}.
|
||||||
//
|
func emitJSONErr(w io.Writer, code, msg string) {
|
||||||
// {"error": {"code": "...", "message": "..."}}
|
enc := json.NewEncoder(w)
|
||||||
//
|
|
||||||
// Returns the supplied exit code so callers can do `return emitJSONErr(...)`.
|
|
||||||
func emitJSONErr(code int, kind, msg string) int {
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
_ = enc.Encode(map[string]any{
|
_ = enc.Encode(map[string]any{
|
||||||
"error": map[string]any{
|
"error": map[string]string{"code": code, "message": msg},
|
||||||
"code": code,
|
|
||||||
"kind": kind,
|
|
||||||
"message": msg,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// failure routes an error to either stdout-as-JSON (when the user passed
|
|
||||||
// -json) or stderr-as-text (default). Returns the supplied exit code.
|
|
||||||
//
|
|
||||||
// kind is a stable short string ("network", "integrity", "verify",
|
|
||||||
// "usage", "internal") — agents can branch on this without parsing
|
|
||||||
// human-readable text.
|
|
||||||
func failure(asJSON bool, code int, kind, prefix string, err error) int {
|
|
||||||
msg := err.Error()
|
|
||||||
if prefix != "" {
|
|
||||||
msg = prefix + ": " + msg
|
|
||||||
}
|
|
||||||
if asJSON {
|
|
||||||
return emitJSONErr(code, kind, msg)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stderr, msg)
|
|
||||||
return code
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
@ -25,17 +24,17 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/aggregator"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/artifact"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/artifact"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/chain"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/chain"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/manifest"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/manifest"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/mcp"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/mcp"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/networks"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/stm"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/stm"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/verify"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "1.0.1"
|
const version = "0.0.3-dev"
|
||||||
|
|
||||||
// Stable exit codes. Any addition goes at the end; existing values
|
// Stable exit codes. Any addition goes at the end; existing values
|
||||||
// don't renumber. LLM/automation-friendly contract.
|
// don't renumber. LLM/automation-friendly contract.
|
||||||
|
|
@ -133,12 +132,14 @@ func cmdList(ctx context.Context, args []string) int {
|
||||||
asJSON := fs.Bool("json", false, "emit structured JSON")
|
asJSON := fs.Bool("json", false, "emit structured JSON")
|
||||||
n, _, err := resolveNetwork(fs, args)
|
n, _, err := resolveNetwork(fs, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitUsage, "usage", "", err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return 2
|
||||||
}
|
}
|
||||||
c := aggregator.New(n.AggregatorURL)
|
c := aggregator.New(n.AggregatorURL)
|
||||||
snaps, err := c.ListCardanoDBSnapshots(ctx)
|
snaps, err := c.ListCardanoDBSnapshots(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitNetwork, "network", "list", err)
|
fmt.Fprintln(os.Stderr, "list:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if *asJSON {
|
if *asJSON {
|
||||||
return emitJSON(map[string]any{
|
return emitJSON(map[string]any{
|
||||||
|
|
@ -166,7 +167,8 @@ func cmdShow(ctx context.Context, args []string) int {
|
||||||
asJSON := fs.Bool("json", false, "emit structured JSON")
|
asJSON := fs.Bool("json", false, "emit structured JSON")
|
||||||
n, rest, err := resolveNetwork(fs, args)
|
n, rest, err := resolveNetwork(fs, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitUsage, "usage", "", err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return exitUsage
|
||||||
}
|
}
|
||||||
hash := "latest"
|
hash := "latest"
|
||||||
if len(rest) > 0 {
|
if len(rest) > 0 {
|
||||||
|
|
@ -175,7 +177,8 @@ func cmdShow(ctx context.Context, args []string) int {
|
||||||
c := aggregator.New(n.AggregatorURL)
|
c := aggregator.New(n.AggregatorURL)
|
||||||
snap, err := resolveSnapshot(ctx, c, hash)
|
snap, err := resolveSnapshot(ctx, c, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitNetwork, "network", "show", err)
|
fmt.Fprintln(os.Stderr, "show:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if *asJSON {
|
if *asJSON {
|
||||||
return emitJSON(snap)
|
return emitJSON(snap)
|
||||||
|
|
@ -279,11 +282,12 @@ func cmdVerify(ctx context.Context, args []string) int {
|
||||||
asJSON := fs.Bool("json", false, "emit structured JSON")
|
asJSON := fs.Bool("json", false, "emit structured JSON")
|
||||||
n, rest, err := resolveNetwork(fs, args)
|
n, rest, err := resolveNetwork(fs, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitUsage, "usage", "", err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return exitUsage
|
||||||
}
|
}
|
||||||
if len(rest) == 0 {
|
if len(rest) == 0 {
|
||||||
return failure(*asJSON, exitUsage, "usage", "",
|
fmt.Fprintln(os.Stderr, "verify: cert hash required (or 'head' / 'genesis')")
|
||||||
fmt.Errorf("verify: cert hash required (or 'head' / 'genesis' / 'chain' / 'manifest <dir>')"))
|
return exitUsage
|
||||||
}
|
}
|
||||||
mode := rest[0] // "head" = verify head cert (STM, not yet), "genesis" = walk chain + verify genesis, or a specific hash
|
mode := rest[0] // "head" = verify head cert (STM, not yet), "genesis" = walk chain + verify genesis, or a specific hash
|
||||||
c := aggregator.New(n.AggregatorURL)
|
c := aggregator.New(n.AggregatorURL)
|
||||||
|
|
@ -305,21 +309,24 @@ func cmdVerify(ctx context.Context, args []string) int {
|
||||||
|
|
||||||
func runVerifyManifest(args []string, asJSON bool) int {
|
func runVerifyManifest(args []string, asJSON bool) int {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return failure(asJSON, exitUsage, "usage", "",
|
fmt.Fprintln(os.Stderr, "verify manifest: needs path to download dir (with digests/ + db/)")
|
||||||
fmt.Errorf("verify manifest: needs path to download dir (with digests/ + db/)"))
|
return exitUsage
|
||||||
}
|
}
|
||||||
dir := args[0]
|
dir := args[0]
|
||||||
digestsPath, err := manifest.LocateDigests(filepath.Join(dir, "digests"))
|
digestsPath, err := manifest.LocateDigests(filepath.Join(dir, "digests"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitGeneric, "internal", "locate digests.json", err)
|
fmt.Fprintln(os.Stderr, "locate digests.json:", err)
|
||||||
|
return exitGeneric
|
||||||
}
|
}
|
||||||
entries, err := manifest.Load(digestsPath)
|
entries, err := manifest.Load(digestsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitIntegrity, "integrity", "load manifest", err)
|
fmt.Fprintln(os.Stderr, "load manifest:", err)
|
||||||
|
return exitIntegrity
|
||||||
}
|
}
|
||||||
res, err := manifest.Verify(entries, filepath.Join(dir, "db"))
|
res, err := manifest.Verify(entries, filepath.Join(dir, "db"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitGeneric, "internal", "verify manifest", err)
|
fmt.Fprintln(os.Stderr, "verify manifest:", err)
|
||||||
|
return exitGeneric
|
||||||
}
|
}
|
||||||
if asJSON {
|
if asJSON {
|
||||||
code := emitJSON(res)
|
code := emitJSON(res)
|
||||||
|
|
@ -342,11 +349,13 @@ func runVerifyChain(ctx context.Context, n networks.Network, asJSON bool) int {
|
||||||
c := aggregator.New(n.AggregatorURL)
|
c := aggregator.New(n.AggregatorURL)
|
||||||
snap, err := resolveSnapshot(ctx, c, "latest")
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "resolve", err)
|
fmt.Fprintln(os.Stderr, "resolve:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
res, err := chain.Verify(ctx, nil, n, snap.CertificateHash, 2048)
|
res, err := chain.Verify(ctx, nil, n, snap.CertificateHash, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "chain verify", err)
|
fmt.Fprintln(os.Stderr, "chain verify:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if asJSON {
|
if asJSON {
|
||||||
code := emitJSON(res)
|
code := emitJSON(res)
|
||||||
|
|
@ -381,19 +390,22 @@ func runVerifyGenesis(ctx context.Context, c *aggregator.Client, n networks.Netw
|
||||||
// Find the head snapshot's cert, walk to genesis, verify Ed25519 on the genesis cert.
|
// Find the head snapshot's cert, walk to genesis, verify Ed25519 on the genesis cert.
|
||||||
snap, err := resolveSnapshot(ctx, c, "latest")
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "resolve", err)
|
fmt.Fprintln(os.Stderr, "resolve:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
certs, err := c.CertChain(ctx, snap.CertificateHash, 2048)
|
chain, err := c.CertChain(ctx, snap.CertificateHash, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "chain", err)
|
fmt.Fprintln(os.Stderr, "chain:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if len(certs) == 0 {
|
if len(chain) == 0 {
|
||||||
return failure(asJSON, exitGeneric, "internal", "", fmt.Errorf("empty chain"))
|
fmt.Fprintln(os.Stderr, "empty chain")
|
||||||
|
return exitGeneric
|
||||||
}
|
}
|
||||||
gen := certs[len(certs)-1]
|
gen := chain[len(chain)-1]
|
||||||
if gen.GenesisSignature == "" {
|
if gen.GenesisSignature == "" {
|
||||||
return failure(asJSON, exitGeneric, "internal", "",
|
fmt.Fprintln(os.Stderr, "tail of chain is not a genesis certificate")
|
||||||
fmt.Errorf("tail of chain is not a genesis certificate"))
|
return exitGeneric
|
||||||
}
|
}
|
||||||
return verifyGenesisCert(n, gen, asJSON)
|
return verifyGenesisCert(n, gen, asJSON)
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +413,8 @@ func runVerifyGenesis(ctx context.Context, c *aggregator.Client, n networks.Netw
|
||||||
func runVerifyHead(ctx context.Context, c *aggregator.Client, n networks.Network, asJSON bool) int {
|
func runVerifyHead(ctx context.Context, c *aggregator.Client, n networks.Network, asJSON bool) int {
|
||||||
snap, err := resolveSnapshot(ctx, c, "latest")
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "resolve", err)
|
fmt.Fprintln(os.Stderr, "resolve:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
return runVerifySingle(ctx, c, n, snap.CertificateHash, asJSON)
|
return runVerifySingle(ctx, c, n, snap.CertificateHash, asJSON)
|
||||||
}
|
}
|
||||||
|
|
@ -409,7 +422,8 @@ func runVerifyHead(ctx context.Context, c *aggregator.Client, n networks.Network
|
||||||
func runVerifySingle(ctx context.Context, c *aggregator.Client, n networks.Network, hash string, asJSON bool) int {
|
func runVerifySingle(ctx context.Context, c *aggregator.Client, n networks.Network, hash string, asJSON bool) int {
|
||||||
cert, err := c.GetCertificate(ctx, hash)
|
cert, err := c.GetCertificate(ctx, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "cert", err)
|
fmt.Fprintln(os.Stderr, "cert:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if cert.GenesisSignature != "" {
|
if cert.GenesisSignature != "" {
|
||||||
return verifyGenesisCert(n, cert, asJSON)
|
return verifyGenesisCert(n, cert, asJSON)
|
||||||
|
|
@ -424,15 +438,18 @@ func verifySTMCert(ctx context.Context, c *aggregator.Client, n networks.Network
|
||||||
// Re-fetch as raw JSON to access the AVK + params fields.
|
// Re-fetch as raw JSON to access the AVK + params fields.
|
||||||
raw, err := fetchCertRaw(ctx, n.AggregatorURL, hash)
|
raw, err := fetchCertRaw(ctx, n.AggregatorURL, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitNetwork, "network", "fetch raw cert", err)
|
fmt.Fprintln(os.Stderr, "fetch raw cert:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
ms, err := stm.DecodeMultiSig(raw.MultiSignature)
|
ms, err := stm.DecodeMultiSig(raw.MultiSignature)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitIntegrity, "integrity", "decode multi_signature", err)
|
fmt.Fprintln(os.Stderr, "decode multi_signature:", err)
|
||||||
|
return exitIntegrity
|
||||||
}
|
}
|
||||||
avk, err := stm.DecodeAVK(raw.AggregateVerificationKey)
|
avk, err := stm.DecodeAVK(raw.AggregateVerificationKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitIntegrity, "integrity", "decode avk", err)
|
fmt.Fprintln(os.Stderr, "decode avk:", err)
|
||||||
|
return exitIntegrity
|
||||||
}
|
}
|
||||||
msg := []byte(cert.SignedMessage)
|
msg := []byte(cert.SignedMessage)
|
||||||
params := stm.Parameters{K: raw.Metadata.Parameters.K, M: raw.Metadata.Parameters.M, PhiF: raw.Metadata.Parameters.PhiF}
|
params := stm.Parameters{K: raw.Metadata.Parameters.K, M: raw.Metadata.Parameters.M, PhiF: raw.Metadata.Parameters.PhiF}
|
||||||
|
|
@ -484,21 +501,14 @@ func fetchCertRaw(ctx context.Context, aggregatorURL, hash string) (*rawCert, er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
||||||
return nil, fmt.Errorf("aggregator GET /certificate/%s: %d: %s", hash, resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
// Cap at 16 MiB — current mainnet cert JSON is well under 100 KiB.
|
|
||||||
limited := io.LimitReader(resp.Body, 16<<20)
|
|
||||||
var r rawCert
|
var r rawCert
|
||||||
if err := json.NewDecoder(limited).Decode(&r); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
||||||
return nil, fmt.Errorf("decode cert json: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
return &r, nil
|
return &r, nil
|
||||||
}
|
}
|
||||||
|
|
@ -506,7 +516,8 @@ func fetchCertRaw(ctx context.Context, aggregatorURL, hash string) (*rawCert, er
|
||||||
func verifyGenesisCert(n networks.Network, cert *aggregator.Certificate, asJSON bool) int {
|
func verifyGenesisCert(n networks.Network, cert *aggregator.Certificate, asJSON bool) int {
|
||||||
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
|
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(asJSON, exitGeneric, "internal", "decode genesis key", err)
|
fmt.Fprintln(os.Stderr, "decode genesis key:", err)
|
||||||
|
return exitGeneric
|
||||||
}
|
}
|
||||||
err = verify.GenesisFromJSON(vk, cert.SignedMessage, cert.GenesisSignature, cert.ProtocolMessage)
|
err = verify.GenesisFromJSON(vk, cert.SignedMessage, cert.GenesisSignature, cert.ProtocolMessage)
|
||||||
if asJSON {
|
if asJSON {
|
||||||
|
|
@ -541,25 +552,28 @@ func cmdCert(ctx context.Context, args []string) int {
|
||||||
asJSON := fs.Bool("json", false, "emit structured JSON")
|
asJSON := fs.Bool("json", false, "emit structured JSON")
|
||||||
n, rest, err := resolveNetwork(fs, args)
|
n, rest, err := resolveNetwork(fs, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitUsage, "usage", "", err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return exitUsage
|
||||||
}
|
}
|
||||||
if len(rest) == 0 {
|
if len(rest) == 0 {
|
||||||
return failure(*asJSON, exitUsage, "usage", "",
|
fmt.Fprintln(os.Stderr, "cert: hash required (or 'head' to use the latest snapshot's cert_hash)")
|
||||||
fmt.Errorf("cert: hash required (or 'head' to use the latest snapshot's cert_hash)"))
|
return exitUsage
|
||||||
}
|
}
|
||||||
head := rest[0]
|
head := rest[0]
|
||||||
c := aggregator.New(n.AggregatorURL)
|
c := aggregator.New(n.AggregatorURL)
|
||||||
if head == "head" {
|
if head == "head" {
|
||||||
snap, err := resolveSnapshot(ctx, c, "latest")
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitNetwork, "network", "resolve head", err)
|
fmt.Fprintln(os.Stderr, "resolve head:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
head = snap.CertificateHash
|
head = snap.CertificateHash
|
||||||
}
|
}
|
||||||
if *chain {
|
if *chain {
|
||||||
certs, err := c.CertChain(ctx, head, *maxDepth)
|
certs, err := c.CertChain(ctx, head, *maxDepth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitNetwork, "network", "chain", err)
|
fmt.Fprintln(os.Stderr, "chain:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if *asJSON {
|
if *asJSON {
|
||||||
return emitJSON(map[string]any{"chain_length": len(certs), "certs": certs})
|
return emitJSON(map[string]any{"chain_length": len(certs), "certs": certs})
|
||||||
|
|
@ -577,7 +591,8 @@ func cmdCert(ctx context.Context, args []string) int {
|
||||||
}
|
}
|
||||||
cert, err := c.GetCertificate(ctx, head)
|
cert, err := c.GetCertificate(ctx, head)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitNetwork, "network", "cert", err)
|
fmt.Fprintln(os.Stderr, "cert:", err)
|
||||||
|
return exitNetwork
|
||||||
}
|
}
|
||||||
if *asJSON {
|
if *asJSON {
|
||||||
return emitJSON(cert)
|
return emitJSON(cert)
|
||||||
|
|
@ -604,7 +619,8 @@ func cmdInfo(args []string) int {
|
||||||
asJSON := fs.Bool("json", false, "emit structured JSON")
|
asJSON := fs.Bool("json", false, "emit structured JSON")
|
||||||
n, _, err := resolveNetwork(fs, args)
|
n, _, err := resolveNetwork(fs, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failure(*asJSON, exitUsage, "usage", "", err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return exitUsage
|
||||||
}
|
}
|
||||||
if *asJSON {
|
if *asJSON {
|
||||||
return emitJSON(map[string]any{
|
return emitJSON(map[string]any{
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/aggregator"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/chain"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/chain"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/mcp"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/mcp"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/networks"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/stm"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/stm"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/verify"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
func errString(e error) string {
|
func errString(e error) string {
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,4 +1,4 @@
|
||||||
module git.sulkta.com/Sulkta-Coop/mithril-go
|
module git.sulkta.coop/Sulkta-Coop/mithril-go
|
||||||
|
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,6 @@ func Download(ctx context.Context, uri, destPath, expectedSHA256 string, progres
|
||||||
if expectedSHA256 != "" {
|
if expectedSHA256 != "" {
|
||||||
got := hex.EncodeToString(h.Sum(nil))
|
got := hex.EncodeToString(h.Sum(nil))
|
||||||
if got != expectedSHA256 {
|
if got != expectedSHA256 {
|
||||||
// Remove the .part file — leaving it behind would cause every
|
|
||||||
// subsequent retry to resume from the same corrupted bytes and
|
|
||||||
// fail SHA again indefinitely.
|
|
||||||
_ = os.Remove(partPath)
|
|
||||||
return fmt.Errorf("SHA256 mismatch: want %s, got %s", expectedSHA256, got)
|
return fmt.Errorf("SHA256 mismatch: want %s, got %s", expectedSHA256, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/aggregator"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/networks"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/stm"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/stm"
|
||||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/verify"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step is the per-cert record in a chain-verification report.
|
// Step is the per-cert record in a chain-verification report.
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,6 @@ func IsLotteryWon(phiF float64, ev [64]byte, stake, totalStake uint64) bool {
|
||||||
if math.Abs(phiF-1.0) < 1e-15 {
|
if math.Abs(phiF-1.0) < 1e-15 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Defensive: zero-stake or zero-total-stake produces nonsense (and
|
|
||||||
// totalStake==0 would panic at SetFrac). Guard at the lottery layer
|
|
||||||
// in addition to AVK-decode-time validation.
|
|
||||||
if stake == 0 || totalStake == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ev as big int (LE interpretation)
|
// ev as big int (LE interpretation)
|
||||||
evInt := evAsBigInt(ev)
|
evInt := evAsBigInt(ev)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"golang.org/x/crypto/blake2b"
|
"golang.org/x/crypto/blake2b"
|
||||||
)
|
)
|
||||||
|
|
@ -77,31 +78,16 @@ func VerifyMerkleBatch(root []byte, nrLeaves int, leafValues [][]byte, indices [
|
||||||
if len(leafValues) != len(indices) {
|
if len(leafValues) != len(indices) {
|
||||||
return fmt.Errorf("leaves/indices count mismatch: %d vs %d", len(leafValues), len(indices))
|
return fmt.Errorf("leaves/indices count mismatch: %d vs %d", len(leafValues), len(indices))
|
||||||
}
|
}
|
||||||
if nrLeaves <= 0 {
|
// Must be sorted ascending
|
||||||
return fmt.Errorf("nrLeaves must be positive, got %d", nrLeaves)
|
|
||||||
}
|
|
||||||
// Validate every proof node is a 32-byte BLAKE2b-256 digest. Anything
|
|
||||||
// shorter or longer is malformed and Rust would reject it.
|
|
||||||
for i, v := range proofValues {
|
|
||||||
if len(v) != 32 {
|
|
||||||
return fmt.Errorf("proof value [%d]: got %d bytes, want 32", i, len(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Indices must be strictly ascending — duplicates would create
|
|
||||||
// double-claiming under the same leaf and the algorithm doesn't expect
|
|
||||||
// them. (Rust uses sort_unstable + equality compare against the input;
|
|
||||||
// equivalent to "non-decreasing" but doesn't reject equal-adjacent.
|
|
||||||
// We're stricter than upstream here on purpose.)
|
|
||||||
ordered := make([]int, len(indices))
|
ordered := make([]int, len(indices))
|
||||||
for i, v := range indices {
|
for i, v := range indices {
|
||||||
if v >= uint64(nrLeaves) {
|
|
||||||
return fmt.Errorf("index [%d]=%d out of range (nr_leaves=%d)", i, v, nrLeaves)
|
|
||||||
}
|
|
||||||
ordered[i] = int(v)
|
ordered[i] = int(v)
|
||||||
}
|
}
|
||||||
for i := 1; i < len(ordered); i++ {
|
sortedCopy := append([]int(nil), ordered...)
|
||||||
if ordered[i] <= ordered[i-1] {
|
sort.Ints(sortedCopy)
|
||||||
return fmt.Errorf("indices not strictly ascending at [%d]: %v", i, indices)
|
for i := range ordered {
|
||||||
|
if ordered[i] != sortedCopy[i] {
|
||||||
|
return fmt.Errorf("indices not sorted ascending: %v", indices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,12 +155,6 @@ func VerifyMerkleBatch(root []byte, nrLeaves int, leafValues [][]byte, indices [
|
||||||
if len(currentLayer) != 1 {
|
if len(currentLayer) != 1 {
|
||||||
return fmt.Errorf("verification ended with %d nodes, want 1", len(currentLayer))
|
return fmt.Errorf("verification ended with %d nodes, want 1", len(currentLayer))
|
||||||
}
|
}
|
||||||
// All proof values must be consumed. Trailing bytes mean the proof
|
|
||||||
// shipped extra nodes the algorithm didn't need — likely malformed
|
|
||||||
// or attacker-padded.
|
|
||||||
if len(values) > 0 {
|
|
||||||
return fmt.Errorf("proof has %d unconsumed values — malformed", len(values))
|
|
||||||
}
|
|
||||||
if !bytes.Equal(currentLayer[0], root) {
|
if !bytes.Equal(currentLayer[0], root) {
|
||||||
return fmt.Errorf("root mismatch: got %x, want %x", currentLayer[0], root)
|
return fmt.Errorf("root mismatch: got %x, want %x", currentLayer[0], root)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
// 4. Lottery check: for each (index, sigma), evaluate_dense_mapping < threshold(stake)
|
// 4. Lottery check: for each (index, sigma), evaluate_dense_mapping < threshold(stake)
|
||||||
// 5. Threshold: total distinct lottery wins >= k
|
// 5. Threshold: total distinct lottery wins >= k
|
||||||
//
|
//
|
||||||
// Phases 2-5 are implemented in verify.go (BLS, lottery, Merkle, threshold).
|
// Phases 2-5 are stubbed in verify.go pending the BLS crypto sprint.
|
||||||
|
// This package's current role is rock-solid decoding.
|
||||||
package stm
|
package stm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -115,12 +116,6 @@ func DecodeAVK(rawJSON []byte) (*AVK, error) {
|
||||||
if len(wire.MTCommitment.Root) != 32 {
|
if len(wire.MTCommitment.Root) != 32 {
|
||||||
return nil, fmt.Errorf("AVK root: got %d bytes, want 32", len(wire.MTCommitment.Root))
|
return nil, fmt.Errorf("AVK root: got %d bytes, want 32", len(wire.MTCommitment.Root))
|
||||||
}
|
}
|
||||||
if wire.TotalStake == 0 {
|
|
||||||
return nil, fmt.Errorf("AVK total_stake is zero")
|
|
||||||
}
|
|
||||||
if wire.MTCommitment.NrLeaves == 0 {
|
|
||||||
return nil, fmt.Errorf("AVK nr_leaves is zero")
|
|
||||||
}
|
|
||||||
return &AVK{
|
return &AVK{
|
||||||
MerkleRoot: wire.MTCommitment.Root,
|
MerkleRoot: wire.MTCommitment.Root,
|
||||||
NumLeaves: wire.MTCommitment.NrLeaves,
|
NumLeaves: wire.MTCommitment.NrLeaves,
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
// 2. Subsequent certificates carry an STM (Stake-based Threshold Multi-
|
// 2. Subsequent certificates carry an STM (Stake-based Threshold Multi-
|
||||||
// signature) aggregate signature over BLS12-381.
|
// signature) aggregate signature over BLS12-381.
|
||||||
//
|
//
|
||||||
// Ed25519 (genesis) verification lives here. STM BLS verification is in
|
// Ed25519 (genesis) verification is fully implemented here. STM verification
|
||||||
// the sibling internal/stm package.
|
// is stubbed pending the BLS crypto sprint.
|
||||||
package verify
|
package verify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -27,6 +27,7 @@ var (
|
||||||
ErrNotGenesis = errors.New("certificate is not a genesis certificate")
|
ErrNotGenesis = errors.New("certificate is not a genesis certificate")
|
||||||
ErrBadSignature = errors.New("genesis signature verification failed")
|
ErrBadSignature = errors.New("genesis signature verification failed")
|
||||||
ErrSignedMessageHash = errors.New("signed_message does not match SHA256(protocol_message)")
|
ErrSignedMessageHash = errors.New("signed_message does not match SHA256(protocol_message)")
|
||||||
|
ErrSTMNotImplemented = errors.New("STM signature verification not implemented yet")
|
||||||
)
|
)
|
||||||
|
|
||||||
// The Mithril enum order on ProtocolMessagePartKey — BTreeMap iteration
|
// The Mithril enum order on ProtocolMessagePartKey — BTreeMap iteration
|
||||||
|
|
@ -181,5 +182,9 @@ func GenesisFromJSON(verifyKey ed25519.PublicKey, signedMessageHex, genesisSigna
|
||||||
return Genesis(verifyKey, signedMessageHex, genesisSignatureHex, pm)
|
return Genesis(verifyKey, signedMessageHex, genesisSignatureHex, pm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// STM verification lives in the sibling internal/stm package — see
|
// STM verifies a non-genesis certificate's aggregate BLS signature.
|
||||||
// stm.Verify(). This file is genesis-Ed25519-only.
|
// Stub — target is Mithril STM paper §5 (signing) + §6 (aggregation)
|
||||||
|
// using gnark-crypto's bls12-381 primitives.
|
||||||
|
func STM(protocolMessageJSON, multiSignature []byte, avk any) error {
|
||||||
|
return ErrSTMNotImplemented
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue