aboutsummaryrefslogtreecommitdiff
path: root/internal/merkle
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-26 15:41:45 -0800
committerFuwn <[email protected]>2026-02-26 15:41:45 -0800
commitfec9114caaa7d274e524793d5eb0cf2ef2c5af11 (patch)
tree0897394ccdfaf6633e1a4ca8eb02bff49bb93c00 /internal/merkle
parentfeat: add read-only PLC API compatibility endpoints (diff)
downloadplutia-test-fec9114caaa7d274e524793d5eb0cf2ef2c5af11.tar.xz
plutia-test-fec9114caaa7d274e524793d5eb0cf2ef2c5af11.zip
feat: Apply Iku formatting
Diffstat (limited to 'internal/merkle')
-rw-r--r--internal/merkle/tree.go49
-rw-r--r--internal/merkle/tree_test.go7
2 files changed, 56 insertions, 0 deletions
diff --git a/internal/merkle/tree.go b/internal/merkle/tree.go
index ff97217..023b4ec 100644
--- a/internal/merkle/tree.go
+++ b/internal/merkle/tree.go
@@ -12,27 +12,36 @@ type Sibling struct {
func HashLeaf(leaf []byte) []byte {
s := sha256.Sum256(leaf)
+
return s[:]
}
func Root(leaves [][]byte) []byte {
if len(leaves) == 0 {
empty := sha256.Sum256(nil)
+
return empty[:]
}
+
level := cloneLevel(leaves)
+
for len(level) > 1 {
next := make([][]byte, 0, (len(level)+1)/2)
+
for i := 0; i < len(level); i += 2 {
left := level[i]
right := left
+
if i+1 < len(level) {
right = level[i+1]
}
+
next = append(next, hashPair(left, right))
}
+
level = next
}
+
return level[0]
}
@@ -40,15 +49,19 @@ func BuildProof(leaves [][]byte, index int) []Sibling {
if len(leaves) == 0 || index < 0 || index >= len(leaves) {
return nil
}
+
proof := make([]Sibling, 0)
level := cloneLevel(leaves)
idx := index
+
for len(level) > 1 {
if idx%2 == 0 {
sib := idx + 1
+
if sib >= len(level) {
sib = idx
}
+
proof = append(proof, Sibling{Hash: hex.EncodeToString(level[sib]), Left: false})
} else {
sib := idx - 1
@@ -56,44 +69,57 @@ func BuildProof(leaves [][]byte, index int) []Sibling {
}
next := make([][]byte, 0, (len(level)+1)/2)
+
for i := 0; i < len(level); i += 2 {
left := level[i]
right := left
+
if i+1 < len(level) {
right = level[i+1]
}
+
next = append(next, hashPair(left, right))
}
+
idx /= 2
level = next
}
+
return proof
}
func VerifyProof(leafHash []byte, proof []Sibling, root []byte) bool {
cur := append([]byte(nil), leafHash...)
+
for _, s := range proof {
sib, err := hex.DecodeString(s.Hash)
+
if err != nil {
return false
}
+
var combined []byte
+
if s.Left {
combined = append(append([]byte(nil), sib...), cur...)
} else {
combined = append(append([]byte(nil), cur...), sib...)
}
+
h := sha256.Sum256(combined)
cur = h[:]
}
+
return hex.EncodeToString(cur) == hex.EncodeToString(root)
}
func cloneLevel(in [][]byte) [][]byte {
out := make([][]byte, len(in))
+
for i := range in {
out[i] = append([]byte(nil), in[i]...)
}
+
return out
}
@@ -110,19 +136,26 @@ func (a *Accumulator) AddLeafHash(leafHash []byte) {
if len(leafHash) == 0 {
return
}
+
cur := append([]byte(nil), leafHash...)
level := 0
+
for {
if level >= len(a.levels) {
a.levels = append(a.levels, nil)
}
+
if a.levels[level] == nil {
a.levels[level] = cur
+
a.pendingCount++
+
break
}
+
cur = hashPair(a.levels[level], cur)
a.levels[level] = nil
+
a.pendingCount--
level++
}
@@ -131,40 +164,53 @@ func (a *Accumulator) AddLeafHash(leafHash []byte) {
func (a *Accumulator) RootDuplicateLast() []byte {
if a.pendingCount == 0 {
empty := sha256.Sum256(nil)
+
return empty[:]
}
+
clone := &Accumulator{
levels: cloneLevel(a.levels),
pendingCount: a.pendingCount,
}
+
for clone.pendingCount > 1 {
level := clone.lowestPendingLevel()
cur := clone.levels[level]
clone.levels[level] = nil
+
clone.pendingCount--
cur = hashPair(cur, cur)
+
level++
+
for {
if level >= len(clone.levels) {
clone.levels = append(clone.levels, nil)
}
+
if clone.levels[level] == nil {
clone.levels[level] = cur
+
clone.pendingCount++
+
break
}
+
cur = hashPair(clone.levels[level], cur)
clone.levels[level] = nil
+
clone.pendingCount--
level++
}
}
+
return clone.highestPendingHash()
}
func hashPair(left, right []byte) []byte {
h := sha256.Sum256(append(append([]byte(nil), left...), right...))
+
return h[:]
}
@@ -174,6 +220,7 @@ func (a *Accumulator) lowestPendingLevel() int {
return i
}
}
+
return 0
}
@@ -183,6 +230,8 @@ func (a *Accumulator) highestPendingHash() []byte {
return a.levels[i]
}
}
+
empty := sha256.Sum256(nil)
+
return empty[:]
}
diff --git a/internal/merkle/tree_test.go b/internal/merkle/tree_test.go
index 9fa791d..b2546a8 100644
--- a/internal/merkle/tree_test.go
+++ b/internal/merkle/tree_test.go
@@ -9,12 +9,14 @@ import (
func TestRootAndProof(t *testing.T) {
inputs := [][]byte{HashLeaf([]byte("a")), HashLeaf([]byte("b")), HashLeaf([]byte("c"))}
root := Root(inputs)
+
if len(root) == 0 {
t.Fatalf("expected root")
}
for i := range inputs {
proof := BuildProof(inputs, i)
+
if !VerifyProof(inputs[i], proof, root) {
t.Fatalf("proof verification failed for index %d", i)
}
@@ -23,6 +25,7 @@ func TestRootAndProof(t *testing.T) {
func TestRootEmpty(t *testing.T) {
r := Root(nil)
+
if got := hex.EncodeToString(r); got != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
t.Fatalf("unexpected empty root: %s", got)
}
@@ -32,13 +35,17 @@ func TestAccumulatorRootMatchesRoot(t *testing.T) {
for n := 1; n <= 128; n++ {
leaves := make([][]byte, 0, n)
acc := NewAccumulator()
+
for i := 0; i < n; i++ {
leaf := HashLeaf([]byte(fmt.Sprintf("leaf-%d", i)))
leaves = append(leaves, leaf)
+
acc.AddLeafHash(leaf)
}
+
want := hex.EncodeToString(Root(leaves))
got := hex.EncodeToString(acc.RootDuplicateLast())
+
if got != want {
t.Fatalf("root mismatch n=%d got=%s want=%s", n, got, want)
}