From b62082d73584c40d406829f04ae7efa6bd5ad62f Mon Sep 17 00:00:00 2001 From: agent-ed374b2a16b664d2 Date: Wed, 15 Apr 2026 22:08:25 +0200 Subject: [PATCH] build(agent): molt-x#ed374b iteration --- .gitignore | 21 ++++++++++ AGENTS.md | 24 +++++++++++ README.md | 31 +++++++++++++- cosmic_ledger/__init__.py | 6 +++ cosmic_ledger/contracts.py | 17 ++++++++ cosmic_ledger/crypto.py | 19 +++++++++ cosmic_ledger/delta.py | 41 +++++++++++++++++++ cosmic_ledger/ledger.py | 82 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 13 ++++++ test.sh | 42 +++++++++++++++++++ tests/test_basic.py | 14 +++++++ 11 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 cosmic_ledger/__init__.py create mode 100644 cosmic_ledger/contracts.py create mode 100644 cosmic_ledger/crypto.py create mode 100644 cosmic_ledger/delta.py create mode 100644 cosmic_ledger/ledger.py create mode 100644 pyproject.toml create mode 100644 test.sh create mode 100644 tests/test_basic.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b113675 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +CosmicLedger SWARM Architecture +- Focus: MVP of offline-first verifiable ledger for partition-tolerant data sharing. +- Core modules: + - cosmic_ledger.contracts: Data contract registry with versioning for mission data types. + - cosmic_ledger.ledger: Local append-only ledger storing entries and signatures. + - cosmic_ledger.delta: Delta logs with Merkle root support and delta export/import. + - cosmic_ledger.crypto: Simple signer/verification using HMAC for MVP correctness. + +- Data model (MVP): + - Telemetry, SensorData, Event, Command, Hazard entries with minimal fields. + - Each entry is cryptographically linked via a signature (node-local signer). + - Merkle DAG roots enable compact proofs of inclusion for entries in the log. + +- Networking (out-of-scope MVP): + - Delta exchange is simulated via in-process delta export/import. + - Adapters/connectors to two domains can be added in future iterations. + +- Tests & Validation: + - test.sh drives a minimal cross-node delta replication scenario to validate consistency. + - AGENTS.md documents intended architectural rules and testing protocol for contributors. + +- Contribution rules: + - Small, well-scoped changes preferred; add tests for new features. + - Update README/docs to reflect changes and how to use the MVP. diff --git a/README.md b/README.md index b29a365..4cedb36 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ -# cosmicledger-verifiable-offline-first-da +CosmicLedger MVP -Problem: Space missions face extreme delays, partitions, and multi-party data sharing across rovers, habitat modules, satellites, and ground stations. Traditional centralized data stores fail under long outages or latency, and data provenance, govern \ No newline at end of file +Overview +- A minimal, offline-first verifiable ledger designed for partitioned networks (e.g., interplanetary missions). +- Local append-only ledger with cryptographic signatures, Merkle-DAG proofs, and delta-based reconciliation. +- A simple Data Contracts Registry to define and version mission data schemas (Telemetry, SensorData, Event, Command, Hazard). +- Delta export/import to synchronize logs between nodes with compact proofs. +- Optional cloud-ground anchoring concept (not implemented in this MVP). + +What you can run here +- A tiny Python package that provides: LocalLedger, DeltaLog, DataContractRegistry, and a tiny Signer using HMAC. +- test.sh script to build and run a basic cross-node delta replication scenario with two ledgers using the same signer key. + +How to use (high level) +- Create a LocalLedger with a node-specific signer key. +- Register data contracts (Telemetry, SensorData, Event, Command, Hazard). +- Append entries to the local ledger. +- Export a delta since a given index and apply it on another ledger to reconcile. +- Compute Merkle roots to verify log integrity. + +Notes +- This is an MVP; cryptographic signing uses HMAC for simplicity in this prototype. In a production setting, you'd replace with Ed25519 or ECDSA and proper key management. +- No external network code is included; delta exchange is simulated via in-process data structures for MVP validation. + +Contributing +- See AGENTS.md for architectural rules and testing protocol. + +License: MIT + +READY_TO_PUBLISH marker is created when this MVP is deemed production-ready in a real SWARM build. diff --git a/cosmic_ledger/__init__.py b/cosmic_ledger/__init__.py new file mode 100644 index 0000000..e80d4bd --- /dev/null +++ b/cosmic_ledger/__init__.py @@ -0,0 +1,6 @@ +from .ledger import LocalLedger +from .contracts import DataContractRegistry +from .delta import DeltaLog, merkle_root +from .crypto import Signer + +__all__ = ["LocalLedger", "DataContractRegistry", "DeltaLog", "merkle_root", "Signer"] diff --git a/cosmic_ledger/contracts.py b/cosmic_ledger/contracts.py new file mode 100644 index 0000000..1942da7 --- /dev/null +++ b/cosmic_ledger/contracts.py @@ -0,0 +1,17 @@ +import json + +class DataContractRegistry: + def __init__(self): + self._contracts = {} + + def register(self, name: str, schema: dict, version: int = 1): + self._contracts[name] = { + "version": version, + "schema": schema, + } + + def get(self, name: str): + return self._contracts.get(name) + + def list_contracts(self): + return {k: v for k, v in self._contracts.items()} diff --git a/cosmic_ledger/crypto.py b/cosmic_ledger/crypto.py new file mode 100644 index 0000000..4b560c7 --- /dev/null +++ b/cosmic_ledger/crypto.py @@ -0,0 +1,19 @@ +import hmac +import hashlib + +class Signer: + """Simple HMAC-based signer for MVP. In a real system, replace with public-key cryptography.""" + def __init__(self, key: bytes): + if isinstance(key, str): + key = key.encode('utf-8') + self._key = key + + def sign(self, data: bytes) -> bytes: + return hmac.new(self._key, data, hashlib.sha256).digest() + + def verify(self, data: bytes, signature: bytes) -> bool: + expected = self.sign(data) + return hmac.compare_digest(expected, signature) + +def digest(data: bytes) -> bytes: + return hashlib.sha256(data).digest() diff --git a/cosmic_ledger/delta.py b/cosmic_ledger/delta.py new file mode 100644 index 0000000..fa5db8d --- /dev/null +++ b/cosmic_ledger/delta.py @@ -0,0 +1,41 @@ +import hashlib +import json +from typing import List + +def _hash_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + +def merkle_root(digests: List[str]) -> str: + if not digests: + return "" + level = [bytes.fromhex(d) for d in digests] + while len(level) > 1: + next_level = [] + for i in range(0, len(level), 2): + left = level[i] + right = level[i+1] if i+1 < len(level) else level[i] + next_level.append(hashlib.sha256(left + right).digest()) + level = next_level + return level[0].hex() + +class DeltaLog: + def __init__(self): + self.entries = [] # each entry is a dict with digest and payload + + def add_entry(self, entry: dict) -> str: + # entry must be serializable, and we store a digest for Merkle + payload_bytes = json.dumps(entry, sort_keys=True).encode('utf-8') + digest = hashlib.sha256(payload_bytes).hexdigest() + self.entries.append({ + "digest": digest, + "payload": entry, + }) + return digest + + def delta_from_index(self, index: int) -> List[dict]: + # return full payloads for simplicity (MVP). In a real system you'd return compact digests with proofs. + return [e["payload"] for e in self.entries[index:]] + + def root(self) -> str: + digests = [e["digest"] for e in self.entries] + return merkle_root(digests) diff --git a/cosmic_ledger/ledger.py b/cosmic_ledger/ledger.py new file mode 100644 index 0000000..87283d4 --- /dev/null +++ b/cosmic_ledger/ledger.py @@ -0,0 +1,82 @@ +import json +import time +import os +import hashlib +from .contracts import DataContractRegistry +from .delta import DeltaLog +from .crypto import Signer, digest + +class LedgerEntry: + def __init__(self, entry_id: str, ts: float, entry_type: str, payload: dict, contract_version: int, signer_name: str, signature: bytes): + self.id = entry_id + self.ts = ts + self.entry_type = entry_type + self.payload = payload + self.contract_version = contract_version + self.signer_name = signer_name + self.signature = signature + + def to_dict(self): + return { + "id": self.id, + "ts": self.ts, + "entry_type": self.entry_type, + "payload": self.payload, + "contract_version": self.contract_version, + "signer_name": self.signer_name, + "signature": self.signature.hex(), + } + + @staticmethod + def from_dict(d: dict): + return LedgerEntry( + d["id"], d["ts"], d["entry_type"], d["payload"], d["contract_version"], d["signer_name"], bytes.fromhex(d["signature"]) + ) + + +class LocalLedger: + def __init__(self, signer_key: bytes = None, node_id: str = None): + self.node_id = node_id or (node_id := os.urandom(4).hex()) + self.contracts = DataContractRegistry() + self.delta_log = DeltaLog() + self._entries = [] # store LedgerEntry objects locally + self._signer = Signer(signer_key or os.urandom(32)) + + def register_contract(self, name: str, schema: dict, version: int = 1): + self.contracts.register(name, schema, version) + + def add_entry(self, entry_type: str, payload: dict, signer_name: str = None, contract_name: str = None) -> LedgerEntry: + if not self.contracts.get(entry_type) and contract_name is None: + # Allow ad-hoc entry without contract in MVP for simplicity + contract_version = 1 + else: + contract = self.contracts.get(entry_type) + contract_version = contract["version"] if contract else 1 + ts = time.time() + # Build a lightweight id: hash of type+ts+payload + raw = json.dumps({"type": entry_type, "ts": ts, "payload": payload}, sort_keys=True).encode('utf-8') + entry_id = hashlib.sha256(raw).hexdigest() + payload_bytes = json.dumps(payload, sort_keys=True).encode('utf-8') + signature = self._signer.sign(payload_bytes) + entry = LedgerEntry(entry_id, ts, entry_type, payload, contract_version, signer_name or self.node_id, signature) + self._entries.append(entry) + self.delta_log.add_entry(entry.to_dict()) + return entry + + def export_delta(self, since_index: int = 0) -> list: + # Return serialized entries since index; this is a simple MVP delta export + delta = self.delta_log.delta_from_index(since_index) + return delta + + def import_delta(self, delta_entries: list): + # delta_entries is a list of entry dicts (as produced by export_delta) to merge + for d in delta_entries: + entry = LedgerEntry.from_dict(d) + self._entries.append(entry) + self.delta_log.add_entry(entry.to_dict()) + + def root(self) -> str: + return self.delta_log.root() + + def __len__(self): + return len(self._entries) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..187a700 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "cosmic_ledger_mvp" +version = "0.1.0" +description = "MVP offline-first verifiable ledger for interplanetary missions" +readme = "README.md" +requires-python = ">=3.8" + +[tool.setuptools.packages.find] +where = ["cosmic_ledger"] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..e9e56f7 --- /dev/null +++ b/test.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail +echo "Running CosmicLedger MVP tests (Python)" + +# Ensure Python packaging can build (as per requirements) +python3 -m build >/tmp/build.log 2>&1 || { echo "Build failed. See /tmp/build.log"; tail -n +1 /tmp/build.log; exit 1; } + +# Run a minimal cross-node delta replication test using the Python library +python3 - << 'PY' +from cosmic_ledger.ledger import LocalLedger +import os + +key = b'shared-secret-for-test-1234567890' +nodeA = LocalLedger(signer_key=key, node_id='nodeA') +nodeB = LocalLedger(signer_key=key, node_id='nodeB') + +nodeA.register_contract('Telemetry', {'fields': ['id','ts','source','type','payload']}, version=1) + +# Node A creates a few entries +nodeA.add_entry('Telemetry', {'id':'t1','ts':1.0,'source':'rover-1','type':'temp','payload':'22C'}, signer_name='rover-1') +nodeA.add_entry('Telemetry', {'id':'t2','ts':2.0,'source':'rover-1','type':'temp','payload':'23C'}, signer_name='rover-1') + +rootA = nodeA.root() +print('NodeA root:', rootA) + +# Export delta from A +delta = nodeA.export_delta(0) +print('Delta size:', len(delta)) + +# B imports delta +nodeB.import_delta(delta) + +rootB = nodeB.root() +print('NodeB root after import:', rootB) + +assert len(nodeA) == len(nodeB), 'Node counts should match after replication' +assert rootA == rootB, 'Merkle roots should match after replication' + +print('Delta replication test passed') +PY + +echo "Tests completed." diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..1a30188 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,14 @@ +from cosmic_ledger.ledger import LocalLedger + +def test_basic_replication(): + key = b'same-secret-key' + a = LocalLedger(signer_key=key, node_id='A') + b = LocalLedger(signer_key=key, node_id='B') + a.register_contract('Telemetry', {'fields': ['id','ts','source','type','payload']}, version=1) + a.add_entry('Telemetry', {'id':'e1','ts':1.0,'source':'A','type':'temp','payload':'20C'}, signer_name='A') + a.add_entry('Telemetry', {'id':'e2','ts':2.0,'source':'A','type':'temp','payload':'21C'}, signer_name='A') + + delta = a.export_delta(0) + b.import_delta(delta) + assert len(a) == len(b) + assert a.root() == b.root()