STM full verification landing — milestones C/D/E complete
Implemented the remaining STM verification layers: - internal/stm/lottery.go: EvaluateSigma (Blake2b-512 lottery draw) + IsLotteryWon with Taylor-series threshold comparison (ported from mithril-stm::eligibility), big.Rat-based to match Rust's num_bigint/ num_rational path - internal/stm/merkle.go: Blake2b-256 Merkle batch-proof verification, faithful port of mithril-stm's verify_leaves_membership_from_batch_path including the 'current is left/right child' branch logic and the 1-byte zero pad for missing siblings - internal/stm/verify.go: top-level stm.Verify(msg, ms, avk, params) glues all four checks: k-threshold, lottery, Merkle, BLS aggregate - cmd: 'verify head' now runs full STM verification; JSON output shows signers, wins, params, verified flag - MCP: new 'mithril_verify_certificate' tool dispatches genesis Ed25519 vs STM by cert kind Verified against live networks: mainnet head cert bc00b551… epoch=626 59 signers 1972/16948 wins ✓ mainnet genesis 25acfcfe… epoch=539 Ed25519 ✓ preprod head dd9c4fcb… epoch=284 2 signers 11/100 wins ✓ preprod genesis 69bc3bdf… epoch=196 Ed25519 ✓ This is a consensus-correct pure-Go Mithril client. Single binary, CGo-free, no upstream Rust dependency. Next: full chain verification (walk head → genesis, check continuity).
This commit is contained in:
parent
c1305913c2
commit
5294cf0bfa
7 changed files with 647 additions and 16 deletions
162
internal/stm/merkle.go
Normal file
162
internal/stm/merkle.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package stm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// Mithril's Merkle tree uses Blake2b-256 over leaf-encodings:
|
||||
//
|
||||
// leaf_bytes = vk_96 || stake_be_u64 (104 bytes)
|
||||
// leaf_hash = Blake2b-256(leaf_bytes)
|
||||
// internal = Blake2b-256(left_hash || right_hash)
|
||||
// empty_sib = Blake2b-256(0x00)
|
||||
//
|
||||
// The tree is heap-indexed: root at 0, leaves at next_power_of_two(nr_leaves)-1
|
||||
// through next_power_of_two(nr_leaves)-1 + nr_leaves - 1.
|
||||
|
||||
// blake2b256 returns Blake2b-256(data).
|
||||
func blake2b256(data ...[]byte) []byte {
|
||||
h, _ := blake2b.New256(nil)
|
||||
for _, d := range data {
|
||||
h.Write(d)
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// LeafBytes encodes a (vk, stake) pair as the 104-byte leaf value hashed
|
||||
// into the Merkle tree.
|
||||
func LeafBytes(vk []byte, stake uint64) []byte {
|
||||
out := make([]byte, 104)
|
||||
copy(out[:96], vk)
|
||||
binary.BigEndian.PutUint64(out[96:], stake)
|
||||
return out
|
||||
}
|
||||
|
||||
// nextPowerOfTwo returns the smallest power of two >= n. 0 returns 1.
|
||||
func nextPowerOfTwo(n int) int {
|
||||
if n <= 1 {
|
||||
return 1
|
||||
}
|
||||
p := 1
|
||||
for p < n {
|
||||
p <<= 1
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func mtParent(i int) int { return (i - 1) / 2 }
|
||||
func mtSibling(i int) int {
|
||||
if i%2 == 1 {
|
||||
return i + 1
|
||||
}
|
||||
return i - 1
|
||||
}
|
||||
|
||||
// VerifyMerkleBatch verifies a Mithril batch proof: a set of leaf values at
|
||||
// the given indices are in the tree with the given root. Returns nil on
|
||||
// success.
|
||||
//
|
||||
// Arguments:
|
||||
// - root: 32-byte Merkle root
|
||||
// - nrLeaves: total number of leaves in the tree (from the AVK commitment)
|
||||
// - leafValues: for each proved leaf, its pre-hash bytes (vk||stake)
|
||||
// - indices: the leaf indices (0-based, within the leaf range); must be
|
||||
// sorted ascending and len must equal len(leafValues)
|
||||
// - proofValues: the Merkle path nodes as provided in the batch proof's
|
||||
// `values` field
|
||||
//
|
||||
// The algorithm walks layer-by-layer from leaves to root, consuming
|
||||
// provided values as siblings when the claimed index's sibling is not
|
||||
// itself a claimed leaf. Direct port of
|
||||
// mithril-stm::membership_commitment::merkle_tree::commitment::verify_leaves_membership_from_batch_path.
|
||||
func VerifyMerkleBatch(root []byte, nrLeaves int, leafValues [][]byte, indices []uint64, proofValues [][]byte) error {
|
||||
if len(leafValues) != len(indices) {
|
||||
return fmt.Errorf("leaves/indices count mismatch: %d vs %d", len(leafValues), len(indices))
|
||||
}
|
||||
// Must be sorted ascending
|
||||
ordered := make([]int, len(indices))
|
||||
for i, v := range indices {
|
||||
ordered[i] = int(v)
|
||||
}
|
||||
sortedCopy := append([]int(nil), ordered...)
|
||||
sort.Ints(sortedCopy)
|
||||
for i := range ordered {
|
||||
if ordered[i] != sortedCopy[i] {
|
||||
return fmt.Errorf("indices not sorted ascending: %v", indices)
|
||||
}
|
||||
}
|
||||
|
||||
npo2 := nextPowerOfTwo(nrLeaves)
|
||||
nrNodes := nrLeaves + npo2 - 1
|
||||
|
||||
// Shift leaf positions into tree coordinates.
|
||||
for i := range ordered {
|
||||
ordered[i] += npo2 - 1
|
||||
}
|
||||
|
||||
// Hash each leaf.
|
||||
currentLayer := make([][]byte, len(leafValues))
|
||||
for i, lv := range leafValues {
|
||||
currentLayer[i] = blake2b256(lv)
|
||||
}
|
||||
|
||||
values := append([][]byte(nil), proofValues...)
|
||||
idx := ordered[0]
|
||||
|
||||
emptySiblingHash := blake2b256([]byte{0x00})
|
||||
|
||||
for idx > 0 {
|
||||
newHashes := make([][]byte, 0, len(ordered))
|
||||
newIndices := make([]int, 0, len(ordered))
|
||||
i := 0
|
||||
idx = mtParent(idx)
|
||||
for i < len(ordered) {
|
||||
newIndices = append(newIndices, mtParent(ordered[i]))
|
||||
if ordered[i]&1 == 0 {
|
||||
// Current is a RIGHT child — its sibling (LEFT) comes from proof values.
|
||||
if len(values) == 0 {
|
||||
return fmt.Errorf("proof truncated at ordered[%d]=%d (expected left sibling)", i, ordered[i])
|
||||
}
|
||||
sib := values[0]
|
||||
values = values[1:]
|
||||
newHashes = append(newHashes, blake2b256(sib, currentLayer[i]))
|
||||
} else {
|
||||
// Current is a LEFT child — sibling is RIGHT.
|
||||
sib := mtSibling(ordered[i])
|
||||
switch {
|
||||
case i+1 < len(ordered) && ordered[i+1] == sib:
|
||||
// Sibling is ALSO a claimed leaf already in currentLayer.
|
||||
newHashes = append(newHashes, blake2b256(currentLayer[i], currentLayer[i+1]))
|
||||
i++
|
||||
case sib < nrNodes:
|
||||
// Sibling not claimed but exists; take from proof.
|
||||
if len(values) == 0 {
|
||||
return fmt.Errorf("proof truncated at ordered[%d]=%d (expected right sibling)", i, ordered[i])
|
||||
}
|
||||
s := values[0]
|
||||
values = values[1:]
|
||||
newHashes = append(newHashes, blake2b256(currentLayer[i], s))
|
||||
default:
|
||||
// Right side is beyond tree — empty sibling.
|
||||
newHashes = append(newHashes, blake2b256(currentLayer[i], emptySiblingHash))
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
currentLayer = newHashes
|
||||
ordered = newIndices
|
||||
}
|
||||
|
||||
if len(currentLayer) != 1 {
|
||||
return fmt.Errorf("verification ended with %d nodes, want 1", len(currentLayer))
|
||||
}
|
||||
if !bytes.Equal(currentLayer[0], root) {
|
||||
return fmt.Errorf("root mismatch: got %x, want %x", currentLayer[0], root)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in a new issue