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:
commit
f87b7fc3c4
8 changed files with 450 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/bin/
|
||||||
|
/mithril-go
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
.coverage*
|
||||||
42
README.md
Normal file
42
README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# mithril-go
|
||||||
|
|
||||||
|
Pure-Go client for the Cardano [Mithril](https://mithril.network) protocol.
|
||||||
|
|
||||||
|
Mithril is Cardano's stake-based certified-snapshot system — it lets a new
|
||||||
|
node bootstrap the chain from a cryptographically-verified snapshot
|
||||||
|
instead of replaying every block from genesis.
|
||||||
|
|
||||||
|
The official [`mithril-client`](https://github.com/input-output-hk/mithril)
|
||||||
|
is Rust. This project is a pure-Go reimplementation that produces a single
|
||||||
|
static binary with no runtime dependencies — useful for:
|
||||||
|
|
||||||
|
- Embedding a Mithril bootstrap into Go-based Cardano tooling
|
||||||
|
(alongside `gouroboros`, `dingo`, and friends)
|
||||||
|
- Running on constrained ARM/embedded targets where shipping the Rust
|
||||||
|
binary + its deps is overkill
|
||||||
|
- Operators who prefer a single `go install`-able helper
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Early development — not usable yet.** Current state:
|
||||||
|
|
||||||
|
- [x] Module scaffold, network configs, aggregator REST client
|
||||||
|
- [x] `list` command hits the aggregator and enumerates cardano-database snapshots
|
||||||
|
- [ ] `download` — range-chunk parallel HTTP, SHA-256 integrity, resume
|
||||||
|
- [ ] `extract` — streamed zstd + tar decompression
|
||||||
|
- [ ] Genesis Ed25519 verification (per-network bootstrap key)
|
||||||
|
- [ ] STM BLS12-381 aggregate-signature verification (the hard part)
|
||||||
|
- [ ] Incremental / ancillary artifact support
|
||||||
|
|
||||||
|
## Usage (eventual)
|
||||||
|
|
||||||
|
```
|
||||||
|
mithril-go info -network mainnet
|
||||||
|
mithril-go list -network mainnet
|
||||||
|
mithril-go download -network mainnet -out ./db latest
|
||||||
|
mithril-go verify -network mainnet ./db
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
TBD
|
||||||
147
cmd/mithril-go/main.go
Normal file
147
cmd/mithril-go/main.go
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Command mithril-go is a pure-Go client for the Cardano Mithril protocol.
|
||||||
|
//
|
||||||
|
// It downloads, verifies, and extracts Mithril-certified snapshots of the
|
||||||
|
// Cardano database without requiring the upstream Rust mithril-client.
|
||||||
|
//
|
||||||
|
// Subcommands:
|
||||||
|
// list — list available cardano-database snapshots on an aggregator
|
||||||
|
// download — fetch a snapshot (verify + extract optional)
|
||||||
|
// verify — verify an already-downloaded snapshot
|
||||||
|
// info — show aggregator + network details
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
||||||
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "0.0.1-dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
cmd := os.Args[1]
|
||||||
|
args := os.Args[2:]
|
||||||
|
switch cmd {
|
||||||
|
case "list":
|
||||||
|
os.Exit(cmdList(args))
|
||||||
|
case "download":
|
||||||
|
os.Exit(cmdDownload(args))
|
||||||
|
case "verify":
|
||||||
|
os.Exit(cmdVerify(args))
|
||||||
|
case "info":
|
||||||
|
os.Exit(cmdInfo(args))
|
||||||
|
case "version", "--version", "-v":
|
||||||
|
fmt.Println("mithril-go", version)
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
usage()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", cmd)
|
||||||
|
usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, `mithril-go — pure Go Mithril snapshot client
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
mithril-go <command> [flags]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list List available cardano-database snapshots
|
||||||
|
download Download + verify + extract a snapshot
|
||||||
|
verify Verify an already-downloaded snapshot
|
||||||
|
info Show network + aggregator info
|
||||||
|
version Print version
|
||||||
|
help Show this help
|
||||||
|
|
||||||
|
Common flags:
|
||||||
|
-network mainnet | preprod | preview (default: preprod)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveNetwork(fs *flag.FlagSet, args []string) (networks.Network, []string, error) {
|
||||||
|
networkName := fs.String("network", "preprod", "Cardano network: mainnet | preprod | preview")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return networks.Network{}, nil, err
|
||||||
|
}
|
||||||
|
n, ok := networks.ByName(*networkName)
|
||||||
|
if !ok {
|
||||||
|
return networks.Network{}, nil, fmt.Errorf("unknown network: %s", *networkName)
|
||||||
|
}
|
||||||
|
return n, fs.Args(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdList(args []string) int {
|
||||||
|
fs := flag.NewFlagSet("list", flag.ExitOnError)
|
||||||
|
n, _, err := resolveNetwork(fs, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
client := aggregator.New(n.AggregatorURL)
|
||||||
|
snaps, err := client.ListCardanoDBSnapshots(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "list:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(tw, "HASH\tEPOCH\tIMMUTABLE\tSIZE\tCREATED")
|
||||||
|
for _, s := range snaps {
|
||||||
|
fmt.Fprintf(tw, "%.16s\t%d\t%d\t%s\t%s\n",
|
||||||
|
s.Hash, s.Beacon.Epoch, s.Beacon.ImmutableFileNumber,
|
||||||
|
humanSize(s.TotalDBSizeUncompressed),
|
||||||
|
s.CreatedAt.UTC().Format("2006-01-02 15:04 MST"))
|
||||||
|
}
|
||||||
|
if err := tw.Flush(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "flush:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDownload(args []string) int {
|
||||||
|
fmt.Fprintln(os.Stderr, "download: not yet implemented")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdVerify(args []string) int {
|
||||||
|
fmt.Fprintln(os.Stderr, "verify: not yet implemented")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdInfo(args []string) int {
|
||||||
|
fs := flag.NewFlagSet("info", flag.ExitOnError)
|
||||||
|
n, _, err := resolveNetwork(fs, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
fmt.Printf("network: %s\n", n.Name)
|
||||||
|
fmt.Printf("aggregator: %s\n", n.AggregatorURL)
|
||||||
|
fmt.Printf("genesis verify key: %s…\n", n.GenesisVerifyKey[:16])
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanSize(b uint64) string {
|
||||||
|
const k = 1024
|
||||||
|
if b < k {
|
||||||
|
return fmt.Sprintf("%dB", b)
|
||||||
|
}
|
||||||
|
units := []string{"K", "M", "G", "T"}
|
||||||
|
v := float64(b)
|
||||||
|
u := 0
|
||||||
|
for v >= k && u < len(units)-1 {
|
||||||
|
v /= k
|
||||||
|
u++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f%s", v, units[u])
|
||||||
|
}
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module git.sulkta.coop/Sulkta-Coop/mithril-go
|
||||||
|
|
||||||
|
go 1.26
|
||||||
125
internal/aggregator/client.go
Normal file
125
internal/aggregator/client.go
Normal 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
|
||||||
|
}
|
||||||
27
internal/artifact/download.go
Normal file
27
internal/artifact/download.go
Normal 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
|
||||||
|
}
|
||||||
40
internal/networks/networks.go
Normal file
40
internal/networks/networks.go
Normal 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
61
internal/verify/verify.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue