diff options
| author | Fuwn <[email protected]> | 2026-02-26 15:41:45 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-26 15:41:45 -0800 |
| commit | fec9114caaa7d274e524793d5eb0cf2ef2c5af11 (patch) | |
| tree | 0897394ccdfaf6633e1a4ca8eb02bff49bb93c00 /internal/merkle | |
| parent | feat: add read-only PLC API compatibility endpoints (diff) | |
| download | plutia-test-fec9114caaa7d274e524793d5eb0cf2ef2c5af11.tar.xz plutia-test-fec9114caaa7d274e524793d5eb0cf2ef2c5af11.zip | |
feat: Apply Iku formatting
Diffstat (limited to 'internal/merkle')
| -rw-r--r-- | internal/merkle/tree.go | 49 | ||||
| -rw-r--r-- | internal/merkle/tree_test.go | 7 |
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) } |