mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
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:
@@ -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[:]
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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[:] }
|
||||
Reference in New Issue
Block a user