build(agent): weasel-1#856f80 iteration
This commit is contained in:
parent
f52511d491
commit
59c7171f78
|
|
@ -1,6 +1,7 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from .crypto import Signer
|
||||||
|
|
||||||
def _hash_bytes(data: bytes) -> str:
|
def _hash_bytes(data: bytes) -> str:
|
||||||
return hashlib.sha256(data).hexdigest()
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
@ -166,6 +167,52 @@ class DeltaLog:
|
||||||
digests = [e["digest"] for e in self.entries]
|
digests = [e["digest"] for e in self.entries]
|
||||||
return merkle_root(digests)
|
return merkle_root(digests)
|
||||||
|
|
||||||
|
def export_delta_block(self, since_index: int = 0, signer_key: bytes = None) -> dict:
|
||||||
|
"""Export a compact delta block suitable for DTN-friendly transfer.
|
||||||
|
|
||||||
|
The block contains:
|
||||||
|
- entries: full LedgerEntry dicts augmented with digest, delta_root, merkle_path
|
||||||
|
- digests: list of leaf digests included in the block (in order)
|
||||||
|
- root: merkle root for the block's leaves
|
||||||
|
- anchor: optional anchor recorded on this log
|
||||||
|
- aggregated_sig: HMAC-SHA256 of the root if signer_key is provided (hex)
|
||||||
|
|
||||||
|
This is a lightweight, backwards-compatible representation that
|
||||||
|
recipients can verify using verify_delta_root and (optional) aggregated_sig.
|
||||||
|
"""
|
||||||
|
leaves = [e["digest"] for e in self.entries]
|
||||||
|
# entries since index
|
||||||
|
entries = []
|
||||||
|
for i in range(since_index, len(self.entries)):
|
||||||
|
entry = self.entries[i]
|
||||||
|
path_info = merkle_path_for_index(leaves, i)
|
||||||
|
full_entry = entry["payload"].copy()
|
||||||
|
full_entry.update({
|
||||||
|
"digest": entry["digest"],
|
||||||
|
"delta_root": path_info["root"],
|
||||||
|
"merkle_path": path_info["path"],
|
||||||
|
"index": i,
|
||||||
|
})
|
||||||
|
entries.append(full_entry)
|
||||||
|
|
||||||
|
block_digests = [e["digest"] for e in self.entries[since_index:]]
|
||||||
|
block_root = merkle_root(block_digests)
|
||||||
|
|
||||||
|
aggregated_sig = None
|
||||||
|
if signer_key is not None:
|
||||||
|
signer = Signer(signer_key)
|
||||||
|
# sign the block root bytes
|
||||||
|
aggregated_sig = signer.sign(block_root.encode("utf-8")).hex()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entries": entries,
|
||||||
|
"digests": block_digests,
|
||||||
|
"root": block_root,
|
||||||
|
"anchor": self.anchor,
|
||||||
|
"aggregated_sig": aggregated_sig,
|
||||||
|
"since_index": since_index,
|
||||||
|
}
|
||||||
|
|
||||||
def checkpoint(self, prune_before_index: int = 0) -> dict:
|
def checkpoint(self, prune_before_index: int = 0) -> dict:
|
||||||
"""Create a checkpoint for entries up to prune_before_index (exclusive) and optionally prune them.
|
"""Create a checkpoint for entries up to prune_before_index (exclusive) and optionally prune them.
|
||||||
|
|
||||||
|
|
@ -203,3 +250,39 @@ class DeltaLog:
|
||||||
if digest in cp.get("digests", []):
|
if digest in cp.get("digests", []):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def verify_delta_block(block: dict, signer_key: bytes = None) -> bool:
|
||||||
|
"""Verify the integrity of a delta block.
|
||||||
|
|
||||||
|
Checks performed:
|
||||||
|
- recompute merkle root from provided digests and compare to block['root']
|
||||||
|
- ensure each entry's digest matches the corresponding digest
|
||||||
|
- if signer_key is provided, verify aggregated_sig matches HMAC of root
|
||||||
|
"""
|
||||||
|
if not block:
|
||||||
|
return False
|
||||||
|
digests = block.get("digests", [])
|
||||||
|
computed_root = merkle_root(digests)
|
||||||
|
if computed_root != (block.get("root") or ""):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ensure entries line up with digests
|
||||||
|
entries = block.get("entries", [])
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
if entry.get("digest") != digests[i]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# verify optional aggregated signature
|
||||||
|
if signer_key is not None:
|
||||||
|
sig_hex = block.get("aggregated_sig")
|
||||||
|
if sig_hex is None:
|
||||||
|
return False
|
||||||
|
signer = Signer(signer_key)
|
||||||
|
expected = signer.sign((computed_root or "").encode("utf-8"))
|
||||||
|
try:
|
||||||
|
return expected.hex() == sig_hex
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
# Ensure repository root is on sys.path so tests can import the package
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from cosmic_ledger.delta import DeltaLog, verify_delta_block
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_and_verify_delta_block_with_signature():
|
||||||
|
dl = DeltaLog()
|
||||||
|
# add a few payloads
|
||||||
|
for i in range(4):
|
||||||
|
dl.add_entry({"v": i})
|
||||||
|
|
||||||
|
key = b'unique-test-key-xyz'
|
||||||
|
block = dl.export_delta_block(since_index=0, signer_key=key)
|
||||||
|
|
||||||
|
assert "root" in block
|
||||||
|
assert "digests" in block
|
||||||
|
assert "aggregated_sig" in block
|
||||||
|
assert block["aggregated_sig"] is not None
|
||||||
|
|
||||||
|
# verify should succeed with correct key
|
||||||
|
assert verify_delta_block(block, signer_key=key)
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_delta_block_tamper_detection():
|
||||||
|
dl = DeltaLog()
|
||||||
|
for i in range(3):
|
||||||
|
dl.add_entry({"v": i})
|
||||||
|
|
||||||
|
key = b'another-key-123'
|
||||||
|
block = dl.export_delta_block(since_index=0, signer_key=key)
|
||||||
|
|
||||||
|
# tamper with a digest
|
||||||
|
if block["digests"]:
|
||||||
|
block["digests"][0] = ("0" if block["digests"][0][0] != "0" else "1") + block["digests"][0][1:]
|
||||||
|
|
||||||
|
assert not verify_delta_block(block, signer_key=key)
|
||||||
Loading…
Reference in New Issue