diff --git a/hash/keccak.go b/hash/keccak.go new file mode 100644 index 0000000..677502a --- /dev/null +++ b/hash/keccak.go @@ -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[:] +} diff --git a/merkle/merkle.go b/merkle/merkle.go new file mode 100644 index 0000000..04361a3 --- /dev/null +++ b/merkle/merkle.go @@ -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] +} diff --git a/merkle/merkle_test.go b/merkle/merkle_test.go new file mode 100644 index 0000000..e6a5a3e --- /dev/null +++ b/merkle/merkle_test.go @@ -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[:] }