Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ rayon = "1"
rustc-hash = "2"
tiny-keccak = { version = "2", features = ["keccak"] }
dashmap = { version = "6.1.0", features = ["rayon"] }
memmap2 = "0.9"
sha2 = "0.10"
# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
Expand Down
2 changes: 2 additions & 0 deletions Ix.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,8 @@ public import Ix.CompileM
public import Ix.DecompileM
public import Ix.KernelCheck
public import Ix.Claim
public import Ix.Merkle
public import Ix.AssumptionTree
public import Ix.Commit
public import Ix.Benchmark.Bench
public import Ix.Aiur
Expand Down
193 changes: 193 additions & 0 deletions Ix/AssumptionTree.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
/-
# AssumptionTree: serializable merkle tree over `Address` leaves

Used to recover the leaf set committed to by a conditional claim's
`assumptions` root. The root alone tells the verifier *which* set
was assumed; the AssumptionTree carries the actual leaves so the
verifier can inspect them.

Two construction modes — both produce the same `node`-shaped trees,
differ only in how leaves are arranged:

- `canonical leaves` builds the same shape that `Ix.Merkle.merkleRootCanonical`
hashes, with `padding` nodes wherever odd-leaf padding occurs.
- `join l r` is free-form O(1) composition; result root matches
`Ix.Merkle.merkleJoin`.

## Serialization

Tag4 size 2 under flag 0xE:

```text
[Tag4(0xE, 2) = 0xE2] [body]

body recursive:
leaf(addr): [0x00] [addr:32]
padding: [0x01]
node(l, r): [0x02] [body l] [body r]
```

`padding` represents the zero-sentinel slot used by the canonical
builder to even out odd levels; its root is exactly `zeroAddress`,
matching the bare 32-byte zero that `Ix.Merkle` mixes into odd-level
hashing. Splitting it from `leaf` keeps `leaves` clean (returns only
real leaves, not synthetic padding addresses).
-/

module
public import Ix.Address
public import Ix.Merkle
public import Ix.Ixon

public section

namespace Ix

open Ixon
open Ix.Merkle (leafHash nodeHash zeroAddress merkleJoin)

/-- A merkle tree over `Address` leaves with explicit shape. -/
inductive AssumptionTree where
| leaf (addr : Address)
| padding
| node (left right : AssumptionTree)
deriving BEq, Repr, Inhabited

namespace AssumptionTree

/-- Recursively compute the root hash. -/
partial def root : AssumptionTree → Address
| .leaf addr => leafHash addr
| .padding => zeroAddress
| .node l r => nodeHash l.root r.root

/-- In-order traversal of real leaves (skips `padding`). Iterative
stack-based walk to avoid stack overflow on deep trees. -/
partial def leaves (t : AssumptionTree) : Array Address := Id.run do
let mut acc : Array Address := #[]
let mut stack : Array AssumptionTree := #[t]
while !stack.isEmpty do
let top := stack.back!
stack := stack.pop
match top with
| .leaf a => acc := acc.push a
| .padding => continue
| .node l r =>
-- Push right first so left is processed first (in-order via LIFO).
stack := stack.push r
stack := stack.push l
return acc

/-- True iff `target` appears as a `leaf` somewhere in the tree. -/
partial def contains (t : AssumptionTree) (target : Address) : Bool :=
match t with
| .leaf a => a == target
| .padding => false
| .node l r => l.contains target || r.contains target

/-- Build the canonical sorted+padded merkle tree over a leaf set.
Returns `none` for an empty (post-dedup) leaf set. Matches the
shape committed to by `merkleRootCanonical`. -/
partial def canonical (leaves : Array Address) : Option AssumptionTree :=
let sorted := dedupSorted (leaves.qsort fun a b => compare a b == .lt)
if sorted.isEmpty then
none
else if sorted.size == 1 then
some (.leaf sorted[0]!)
else
some (reduce (sorted.map .leaf))
where
dedupSorted (xs : Array Address) : Array Address := Id.run do
if xs.isEmpty then return #[]
let mut acc : Array Address := #[xs[0]!]
for i in [1:xs.size] do
if !(xs[i]! == xs[i-1]!) then acc := acc.push xs[i]!
return acc
reduce (level : Array AssumptionTree) : AssumptionTree :=
if level.size == 1 then level[0]!
else reduce (pairLevel level)
pairLevel (level : Array AssumptionTree) : Array AssumptionTree := Id.run do
let mut next : Array AssumptionTree := #[]
let mut i := 0
while i < level.size do
let l := level[i]!
let r := if i + 1 < level.size then level[i+1]! else .padding
next := next.push (.node l r)
i := i + 2
return next

/-- Combine two existing subtrees into a new free-form node in O(1). -/
@[inline] def join (l r : AssumptionTree) : AssumptionTree := .node l r

/-- Recursive helper for `merkleProof`. Returns the leaf-to-root path
if `target` is present, else `none`. -/
partial def searchPath (t : AssumptionTree) (target : Address)
: Option (Array (Address × Bool)) :=
match t with
| .leaf a => if a == target then some #[] else none
| .padding => none
| .node l r =>
match l.searchPath target with
| some p => some (p.push (r.root, false))
| none =>
match r.searchPath target with
| some p => some (p.push (l.root, true))
| none => none

/-- Produce a merkle membership path for `target`. Path is in
leaf-to-root order (matches `verifyMerkleProof`). -/
def merkleProof (t : AssumptionTree) (target : Address)
: Option Ix.Merkle.MerklePath := searchPath t target

/-! ## Serialization -/

def FLAG : UInt8 := 0xE
def VARIANT : UInt64 := 2

def BODY_LEAF : UInt8 := 0x00
def BODY_PADDING : UInt8 := 0x01
def BODY_NODE : UInt8 := 0x02

partial def putBody : AssumptionTree → PutM Unit
| .leaf addr => do
putU8 BODY_LEAF
Serialize.put addr
| .padding => do
putU8 BODY_PADDING
| .node l r => do
putU8 BODY_NODE
putBody l
putBody r

def put (t : AssumptionTree) : PutM Unit := do
putTag4 ⟨FLAG, VARIANT⟩
putBody t

partial def getBody : GetM AssumptionTree := do
let tag : UInt8 ← getU8
if tag == BODY_LEAF then
return .leaf (← Serialize.get)
else if tag == BODY_PADDING then
return .padding
else if tag == BODY_NODE then
let l ← getBody
let r ← getBody
return .node l r
else
throw s!"AssumptionTree.getBody: invalid body tag {tag.toNat}"

def get : GetM AssumptionTree := do
let tag ← getTag4
if tag.flag != FLAG || tag.size != VARIANT then
throw s!"AssumptionTree.get: expected Tag4 0xE/2, got {tag.flag.toNat}/{tag.size}"
getBody

def ser (t : AssumptionTree) : ByteArray := runPut (put t)
def de (bytes : ByteArray) : Except String AssumptionTree :=
runGet get bytes

end AssumptionTree

end Ix

end
Loading