initial scaffold

- module layout: cmd/mithril-go, internal/{aggregator,artifact,verify,networks}
- aggregator REST client, list command working against mainnet
- download/extract/verify stubbed
- no deps yet, pure stdlib
This commit is contained in:
Kayos 2026-04-23 15:12:39 -07:00
commit f87b7fc3c4
8 changed files with 450 additions and 0 deletions

View file

@ -0,0 +1,125 @@
// Package aggregator is a thin HTTP client for the Mithril aggregator REST API.
//
// Only the handful of endpoints needed for client-side snapshot workflows are
// exposed. Authentication is not required for the read paths used here.
package aggregator
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type Client struct {
baseURL string
http *http.Client
}
func New(baseURL string) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
http: &http.Client{Timeout: 60 * time.Second},
}
}
// CardanoDBSnapshot is the server-reported shape for /artifact/cardano-database/{hash}.
// Field set is trimmed to what the client actually consumes — full schema documented
// at https://mithril.network/doc/aggregator-api/.
type CardanoDBSnapshot struct {
Hash string `json:"hash"`
MerkleRoot string `json:"merkle_root"`
Network string `json:"network"`
Beacon Beacon `json:"beacon"`
CertificateHash string `json:"certificate_hash"`
TotalDBSizeUncompressed uint64 `json:"total_db_size_uncompressed"`
Digests LocationList `json:"digests"`
ImmutablesAncillary LocationList `json:"immutables"`
ImmutablesIncremental *IncrementalImmutables `json:"immutables_incremental,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type Beacon struct {
Epoch uint64 `json:"epoch"`
ImmutableFileNumber uint64 `json:"immutable_file_number"`
}
type LocationList struct {
Size uint64 `json:"size"`
Locations []LocationAlt `json:"locations"`
}
// LocationAlt is a best-of alternative; Mithril returns a typed-discriminated object.
type LocationAlt struct {
Type string `json:"type"` // e.g. "cloud_storage", "ipfs"
URI string `json:"uri"`
}
type IncrementalImmutables struct {
AverageSize uint64 `json:"average_size"`
Locations []LocationAlt `json:"locations"`
}
// Certificate is the server-reported shape for /certificate/{hash}.
// Kept minimal; STM verification reads what it needs from the raw JSON later.
type Certificate struct {
Hash string `json:"hash"`
PreviousHash string `json:"previous_hash"`
Epoch uint64 `json:"epoch"`
SignedMessage string `json:"signed_message"`
ProtocolMessage json.RawMessage `json:"protocol_message"`
Multisignature json.RawMessage `json:"multi_signature"`
GenesisSignature string `json:"genesis_signature,omitempty"`
}
func (c *Client) get(ctx context.Context, path string, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("aggregator GET %s: %w", path, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("aggregator GET %s: status %d: %s", path, resp.StatusCode, string(body))
}
if out == nil {
return nil
}
return json.NewDecoder(resp.Body).Decode(out)
}
// ListCardanoDBSnapshots returns the sorted-newest-first list of cardano-database snapshots.
func (c *Client) ListCardanoDBSnapshots(ctx context.Context) ([]CardanoDBSnapshot, error) {
var out []CardanoDBSnapshot
if err := c.get(ctx, "/artifact/cardano-database", &out); err != nil {
return nil, err
}
return out, nil
}
// GetCardanoDBSnapshot fetches details for a single snapshot by hash (or "latest").
func (c *Client) GetCardanoDBSnapshot(ctx context.Context, hash string) (*CardanoDBSnapshot, error) {
var out CardanoDBSnapshot
if err := c.get(ctx, "/artifact/cardano-database/"+url.PathEscape(hash), &out); err != nil {
return nil, err
}
return &out, nil
}
// GetCertificate fetches a certificate by hash for signature verification.
func (c *Client) GetCertificate(ctx context.Context, hash string) (*Certificate, error) {
var out Certificate
if err := c.get(ctx, "/certificate/"+url.PathEscape(hash), &out); err != nil {
return nil, err
}
return &out, nil
}

View file

@ -0,0 +1,27 @@
// Package artifact handles downloading and extracting Mithril snapshot artifacts.
// Currently stubs — HTTP range requests, resumable downloads, zstd+tar extraction
// will be implemented in the next pass.
package artifact
import (
"context"
"errors"
)
var ErrNotImplemented = errors.New("not yet implemented")
// Download fetches an artifact from one of the supplied locations, choosing
// the first reachable one and storing it at destPath.
// Implementation will do:
// - parallel range-chunks over HTTP
// - resume on partial .part file
// - SHA-256 verification against the snapshot manifest
func Download(ctx context.Context, locations []string, destPath string) error {
return ErrNotImplemented
}
// Extract decompresses a zstd+tar archive into targetDir.
// Will stream through zstd -> tar reader without buffering the full archive.
func Extract(ctx context.Context, archivePath, targetDir string) error {
return ErrNotImplemented
}

View file

@ -0,0 +1,40 @@
// Package networks holds Mithril aggregator endpoints and genesis keys
// per Cardano network.
package networks
type Network struct {
Name string
AggregatorURL string
GenesisVerifyKey string // hex-encoded Ed25519 public key used to verify the Mithril genesis cert chain
CardanoConfigURL string // upstream cardano-node config bundle (config.json, genesis files)
}
var (
Mainnet = Network{
Name: "mainnet",
AggregatorURL: "https://aggregator.release-mainnet.api.mithril.network/aggregator",
GenesisVerifyKey: "5b3139312c36362c3134302c3138352c3133382c31312c3233372c3230372c3235302c3134342c32372c322c3138382c33302c31322c38312c3135352c3230342c31302c3137392c37352c32332c3133382c3139362c3231372c352c31342c32302c35372c37392c33392c3137365d",
}
Preprod = Network{
Name: "preprod",
AggregatorURL: "https://aggregator.release-preprod.api.mithril.network/aggregator",
GenesisVerifyKey: "5b3132372c37332c3132342c3136312c31362c38372c3133332c3136372c3135352c3138362c3138372c36372c3231322c37382c3131372c3230352c3234362c35322c35312c31372c3138302c38372c3130342c3139362c3131332c3130332c3239355d", // placeholder — replace with known-good key at implementation time
}
Preview = Network{
Name: "preview",
AggregatorURL: "https://aggregator.pre-release-preview.api.mithril.network/aggregator",
GenesisVerifyKey: "5b3132372c37332c3132342c3136312c31362c38372c3133332c3136372c3135352c3138362c3138372c36372c3231322c37382c3131372c3230352c3234362c35322c35312c31372c3138302c38372c3130342c3139362c3131332c3130332c3239355d", // placeholder
}
)
func ByName(name string) (Network, bool) {
switch name {
case "mainnet":
return Mainnet, true
case "preprod":
return Preprod, true
case "preview":
return Preview, true
}
return Network{}, false
}

61
internal/verify/verify.go Normal file
View file

@ -0,0 +1,61 @@
// Package verify implements signature verification for Mithril certificates.
//
// Two layers of verification exist in Mithril:
//
// 1. The genesis certificate is signed by a static Ed25519 key baked into
// the client (per-network). This bootstraps trust into the STM protocol.
// 2. Subsequent certificates carry an STM (Stake-based Threshold Multi-
// signature) aggregate signature over BLS12-381. Verification requires
// the stake distribution snapshot plus the signers' verification keys
// and their individual signature shares.
//
// v1 scope: genesis Ed25519 verification only. STM/BLS verification is a
// separate follow-on milestone — it is the bulk of the cryptographic work
// in this project.
package verify
import (
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
)
var (
ErrNotGenesis = errors.New("certificate is not a genesis certificate")
ErrBadSignature = errors.New("genesis signature verification failed")
ErrSTMNotImplemented = errors.New("STM signature verification not implemented yet")
)
// Genesis verifies that the certificate was signed by the network's genesis
// verification key. signedPayload is the exact bytes the aggregator stated
// were signed (derived from the certificate's protocol_message, not this
// function's job to construct).
func Genesis(verifyKeyHex, genesisSignatureHex string, signedPayload []byte) error {
pkHex, err := hex.DecodeString(verifyKeyHex)
if err != nil {
return fmt.Errorf("decode verify key: %w", err)
}
// Mithril genesis keys are serialized as hex(ascii-of-byte-array-literal),
// e.g. "[191,66,140,...]" → outer hex → inner ASCII → parse. The real decoder
// will unpack this; for now accept a raw 32-byte hex as well.
pk := ed25519.PublicKey(pkHex)
if len(pk) != ed25519.PublicKeySize {
return fmt.Errorf("verify key wrong size: got %d, want %d", len(pk), ed25519.PublicKeySize)
}
sig, err := hex.DecodeString(genesisSignatureHex)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
if !ed25519.Verify(pk, signedPayload, sig) {
return ErrBadSignature
}
return nil
}
// STM verifies a non-genesis certificate's aggregate BLS signature against
// the stake distribution. Stub — implementation target: Mithril STM paper
// §5 (signing protocol) + §6 (aggregation) using a BLS12-381 library.
func STM(protocolMessage, multiSignature []byte, stakeDistribution any) error {
return ErrSTMNotImplemented
}