crypto/merkle: canonical tagged binary Merkle state-root (keccak-256)

Native-Go authority for the Lux VM state roots — the byte-for-byte
reference the GPU accelerators (CUDA/HIP/Metal/Vulkan/WGSL) match.

- crypto/hash/keccak.go: ComputeKeccak256{,Array} via
  sha3.NewLegacyKeccak256 (Ethereum Keccak-256, 0x01 pad) — NOT
  sha3.Sum256 (FIPS-202 SHA3, 0x06 pad), which would diverge from the
  GPU kernels and split consensus.
- crypto/merkle: Root/LeafHash/NodeHash/EmptyRoot. leaf=keccak(0x00|d),
  node=keccak(0x01|L|R), RFC-6962 lone-right promotion, keccak256("")
  empty root. Shape depends only on leaf count -> bit-identical to the
  gpu-kernels lux::merkle::merkle_root spec across all backends.

Reproduces the spec's 7 canonical KAT vectors byte-for-byte
(n=0,1,2,3,4,5,8). CGO_ENABLED=0 clean, pure-Go, luxfi deps only.
Computation + tests ONLY — consensus wiring (fill xvm StandardBlock.Root
+ executor activation gate) is the separate next phase.
This commit is contained in:
zeekay
2026-06-15 13:57:24 -07:00
parent f680d39967
commit fae2d6ad56
3 changed files with 254 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package hash
import (
// Ethereum Keccak-256 (0x01 domain pad), NOT FIPS-202 SHA3-256 (0x06 pad).
// NewLegacyKeccak256 is the legacy (pre-standardization) Keccak that every
// Lux GPU backend and EVM-compatible consumer hashes against; sha3.Sum256
// would diverge byte-for-byte. Do not swap it.
"golang.org/x/crypto/sha3"
)
// KeccakSize is the byte length of a Keccak-256 digest.
const KeccakSize = 32
// ComputeKeccak256Array computes the Ethereum Keccak-256 hash of the
// concatenation of the input byte slices.
func ComputeKeccak256Array(data ...[]byte) [KeccakSize]byte {
h := sha3.NewLegacyKeccak256()
for _, b := range data {
h.Write(b)
}
var out [KeccakSize]byte
h.Sum(out[:0])
return out
}
// ComputeKeccak256 computes the Ethereum Keccak-256 hash of the concatenation
// of the input byte slices.
func ComputeKeccak256(data ...[]byte) []byte {
out := ComputeKeccak256Array(data...)
return out[:]
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package merkle is the canonical native-Go implementation of the keccak256
// tagged binary Merkle state-root fold. It is the authority that the Lux GPU
// state-root accelerators (CUDA, HIP, Metal, Vulkan, WGSL) must reproduce
// byte-for-byte.
//
// The construction follows RFC 6962 (Certificate Transparency):
//
// leaf_hash(d) = keccak256(0x00 ‖ d) // d is a 32-byte element digest
// node_hash(L, R) = keccak256(0x01 ‖ L ‖ R) // L, R are 32-byte child digests
// empty root = keccak256("") // c5d2…a470
//
// The tree is bottom-up binary with RFC 6962 lone-right promotion: on an odd
// level the last node is promoted UNCHANGED (never hashed with itself, never
// duplicated — this avoids the CVE-2012-2459 root-malleability of Bitcoin's
// duplicate-last rule). Each level reduces cnt → ceil(cnt/2). The tree shape
// depends only on the leaf count, never on digest values, so it is reproducible
// bit-for-bit across every backend.
//
// The 0x00/0x01 tags domain-separate leaf and node inputs, closing the
// second-preimage ambiguity an untagged tree carries: a leaf input always
// starts 0x00, a node input always starts 0x01, and the empty root is the hash
// of the zero-length string — three disjoint input domains.
//
// The normative specification (and the C++ CPU reference these bytes match) is
// lux-private/gpu-kernels/backends/common/lux_merkle_fold.hpp.
package merkle
import "github.com/luxfi/crypto/hash"
// Size is the byte length of a Merkle digest (a keccak256 output).
const Size = hash.KeccakSize
const (
leafTag byte = 0x00 // leaf_hash(d) = keccak256(0x00 ‖ d)
nodeTag byte = 0x01 // node_hash(L, R) = keccak256(0x01 ‖ L ‖ R)
)
// LeafHash returns keccak256(0x00 ‖ d), the tagged hash of a 32-byte element
// digest at the leaf level.
func LeafHash(d [Size]byte) [Size]byte {
return hash.ComputeKeccak256Array([]byte{leafTag}, d[:])
}
// NodeHash returns keccak256(0x01 ‖ left ‖ right), the tagged hash of an
// internal node over its two 32-byte children.
func NodeHash(left, right [Size]byte) [Size]byte {
return hash.ComputeKeccak256Array([]byte{nodeTag}, left[:], right[:])
}
// EmptyRoot returns keccak256(""), the root of the empty leaf set.
func EmptyRoot() [Size]byte {
return hash.ComputeKeccak256Array()
}
// Root computes the keccak256 tagged binary Merkle root over leaves, which is a
// dense slice of 32-byte element digests already compacted to the occupied set
// in ascending order.
//
// len == 0 → keccak256("") (EmptyRoot)
// len == 1 → LeafHash(leaves[0]) (single leaf, no internal node)
// len > 1 → bottom-up tagged binary tree with RFC 6962 lone-right promotion
//
// leaves is not modified.
func Root(leaves [][Size]byte) [Size]byte {
n := len(leaves)
if n == 0 {
return EmptyRoot()
}
// Level 0: tag every leaf digest.
level := make([][Size]byte, n)
for i := range leaves {
level[i] = LeafHash(leaves[i])
}
// Reduce level-by-level until one node remains.
for len(level) > 1 {
cnt := len(level)
parents := (cnt + 1) / 2 // ceil(cnt/2)
next := make([][Size]byte, parents)
pairs := cnt / 2 // floor(cnt/2)
for j := 0; j < pairs; j++ {
next[j] = NodeHash(level[2*j], level[2*j+1])
}
if cnt&1 == 1 {
// Lone right node: promote unchanged.
next[parents-1] = level[cnt-1]
}
level = next
}
return level[0]
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package merkle
import (
"encoding/hex"
"testing"
)
// canonicalLeaf reproduces the synthetic leaf digest the normative spec
// (lux_merkle_fold.hpp KAT block) documents: leaf_i[k] = (i*13 + k) & 0xFF.
func canonicalLeaf(i int) [Size]byte {
var d [Size]byte
for k := 0; k < Size; k++ {
d[k] = byte((i*13 + k) & 0xFF)
}
return d
}
// canonicalLeaves returns the first n canonical synthetic leaves.
func canonicalLeaves(n int) [][Size]byte {
leaves := make([][Size]byte, n)
for i := 0; i < n; i++ {
leaves[i] = canonicalLeaf(i)
}
return leaves
}
func hexRoot(t *testing.T, leaves [][Size]byte) string {
t.Helper()
r := Root(leaves)
return hex.EncodeToString(r[:])
}
// TestCanonicalRoots reproduces the ground-truth Merkle roots embedded in the
// normative spec's KAT block, byte-for-byte, over the same synthetic leaves.
//
// Source: lux-private/gpu-kernels/backends/common/lux_merkle_fold.hpp, §"KNOWN-
// ANSWER TEST VECTORS" — the `merkle` rows.
func TestCanonicalRoots(t *testing.T) {
vectors := []struct {
n int
want string
}{
{0, "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"}, // == keccak256("")
{1, "683ea6c3fe5897c14793af914188cf6213ae23055d2db482662fb2d34397b46c"}, // == LeafHash(leaf_0)
{2, "5f53cd7134abee151bf652e66142fd021f08e6f7e6ba5c6c744aa84db37c842a"},
{3, "b5b7ea7803828c9676bc004498ea1f9e9cb9580bddaf06bafc4750686d209d7e"},
{4, "9a443c07f5bdfbf085a73ee3c9177681fb89aae55d0f830de93ee6e8ced5be5f"},
{5, "1328bf1c0aa1e5f69b5151216729310cb3eff2284c2331096abf1f42227d97a0"},
{8, "fa78e524bec74f936f3571ddbcfdae9eacc15d05f3cff7de3ed8a0124356067f"},
}
for _, v := range vectors {
got := hexRoot(t, canonicalLeaves(v.n))
if got != v.want {
t.Errorf("Root(n=%d):\n got %s\n want %s", v.n, got, v.want)
}
}
}
// TestEmptyRoot pins the empty-set root to keccak256("") (the canonical
// Keccak-256 empty-data hash, identical to the EVM empty-data hash). This also
// guards against an accidental sha3.Sum256 (FIPS-202) swap, which would yield
// a different digest.
func TestEmptyRoot(t *testing.T) {
const want = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
if got := hex.EncodeToString(mustBytes(EmptyRoot())); got != want {
t.Errorf("EmptyRoot:\n got %s\n want %s", got, want)
}
// Root over a nil/empty slice must equal EmptyRoot.
if got := hexRoot(t, nil); got != want {
t.Errorf("Root(nil):\n got %s\n want %s", got, want)
}
}
// TestSingleLeaf confirms n==1 returns LeafHash(leaves[0]) with no internal
// node wrapping.
func TestSingleLeaf(t *testing.T) {
const want = "683ea6c3fe5897c14793af914188cf6213ae23055d2db482662fb2d34397b46c"
leaf0 := canonicalLeaf(0)
if got := hex.EncodeToString(mustBytes(LeafHash(leaf0))); got != want {
t.Errorf("LeafHash(leaf_0):\n got %s\n want %s", got, want)
}
if got := hexRoot(t, [][Size]byte{leaf0}); got != want {
t.Errorf("Root([leaf_0]):\n got %s\n want %s", got, want)
}
}
// TestLoneRightPromotion verifies the worked n=3 shape from §3:
// Root = node_hash( node_hash(L0,L1), L2 ), where L2 (the lone leaf hash) is
// promoted UNCHANGED — not hashed with itself, not duplicated.
func TestLoneRightPromotion(t *testing.T) {
leaves := canonicalLeaves(3)
l0 := LeafHash(leaves[0])
l1 := LeafHash(leaves[1])
l2 := LeafHash(leaves[2]) // promoted unchanged at level 0
want := NodeHash(NodeHash(l0, l1), l2)
got := Root(leaves)
if got != want {
t.Errorf("n=3 shape:\n got %x\n want %x", got, want)
}
// Negative control: duplicate-last (Bitcoin CVE-2012-2459) would be
// node_hash( node_hash(L0,L1), node_hash(L2,L2) ) and must NOT match.
dup := NodeHash(NodeHash(l0, l1), NodeHash(l2, l2))
if got == dup {
t.Error("n=3 root matched the rejected duplicate-last shape")
}
}
// TestRootDoesNotMutateInput guards the documented "leaves is not modified"
// contract.
func TestRootDoesNotMutateInput(t *testing.T) {
leaves := canonicalLeaves(5)
before := canonicalLeaves(5)
_ = Root(leaves)
for i := range leaves {
if leaves[i] != before[i] {
t.Fatalf("Root mutated leaves[%d]", i)
}
}
}
func mustBytes(a [Size]byte) []byte { return a[:] }