build(agent): melter#14fd4b iteration
This commit is contained in:
parent
d7824a3d63
commit
e1ac379a47
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
Repository Agent Guide — BlockEnergy Mesh (idea85-blockenergy-mesh-verifiable)
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
- This repository holds a protocol skeleton and a focused Phase-0 implementation for the BlockEnergy Mesh idea. Agents must preserve the registry, primitives, adapters, and the toy proof mechanism. Future agents will extend with solvers, ZK proofs, and ledger integrations.
|
||||||
|
|
||||||
|
Tech stack
|
||||||
|
- Python 3.8+
|
||||||
|
- pydantic for canonical models and schema validation
|
||||||
|
- cryptography for signature-based attestations
|
||||||
|
- numpy for small numerical helpers
|
||||||
|
- pytest for tests
|
||||||
|
|
||||||
|
Key paths
|
||||||
|
- src/idea85_blockenergy_mesh_verifiable/: package root
|
||||||
|
- primitives.py: LocalProblem, SharedVariables, PlanDelta, AuditLog
|
||||||
|
- registry.py: GraphOfContracts (in-memory registry)
|
||||||
|
- adapters.py: sample adapters and adapter base class
|
||||||
|
- proofs.py: toy proof generation and verification
|
||||||
|
|
||||||
|
Testing
|
||||||
|
- Run `bash test.sh`. The script executes `pytest` and `python3 -m build` to validate packaging.
|
||||||
|
|
||||||
|
Git / CI notes for agents
|
||||||
|
- Do not run git commands from the agent unless the user explicitly requests commits or pushes.
|
||||||
|
- When extending interfaces or changing persisted schema, update registry tests and include a migration plan.
|
||||||
|
|
||||||
|
Contribution rules
|
||||||
|
- Keep changes minimal and focused. Follow the repo's dataclass/pydantic patterns.
|
||||||
|
- Add unit tests for any new behavior. Ensure `pytest` remains green.
|
||||||
22
README.md
22
README.md
|
|
@ -1,3 +1,21 @@
|
||||||
# idea85-blockenergy-mesh-verifiable
|
BlockEnergy Mesh — idea85-blockenergy-mesh-verifiable
|
||||||
|
=====================================================
|
||||||
|
|
||||||
Source logic for Idea #85
|
This repository contains a focused, production-oriented chunk of the BlockEnergy Mesh project: a protocol skeleton, canonical primitives, a Graph-of-Contracts registry, two sample adapters, and a toy on-chain proof mechanism (signature-based attestation of constraint satisfaction).
|
||||||
|
|
||||||
|
What is included (MVP slice)
|
||||||
|
- Canonical primitives: LocalProblem, SharedVariables, PlanDelta, AuditLog (pydantic models)
|
||||||
|
- Graph-of-Contracts: simple registry to version contract schemas and adapters
|
||||||
|
- Two starter adapters: SubstationMeterAdapter and DerAggregatorAdapter that map device data to LocalProblem and produce PlanDelta
|
||||||
|
- Toy proof mechanism: cryptographic commitment (SHA-256) + ECDSA signature (via cryptography) as a verifiable attestation
|
||||||
|
- Tests and packaging: pytest tests and pyproject.toml for packaging
|
||||||
|
|
||||||
|
Run tests
|
||||||
|
1. Create a virtualenv (recommended)
|
||||||
|
2. Install test deps (optional): `pip install -r requirements.txt` (not required)
|
||||||
|
3. Run the test script: `bash test.sh`
|
||||||
|
|
||||||
|
This will run `pytest` and `python3 -m build` to validate packaging metadata.
|
||||||
|
|
||||||
|
Roadmap
|
||||||
|
- This patch implements Phase 0 skeleton and a toy proof scheme. Next steps by the swarm: integrate ZK proofs (snarkjs/circom or zksnark bindings), off-chain solver (ADMM-lite), delta-sync reconciliation, governance ledger integration, and adapter conformance suite.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "idea85-blockenergy-mesh-verifiable"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "BlockEnergy Mesh: Verifiable Cross-Utility Microgrid Data Exchange (toy MVP)"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"cryptography>=3.4",
|
||||||
|
"pydantic>=1.8",
|
||||||
|
"numpy>=1.21",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
cryptography>=3.4
|
||||||
|
pydantic>=1.8
|
||||||
|
numpy>=1.21
|
||||||
|
pytest
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""idea85_blockenergy_mesh_verifiable package."""
|
||||||
|
__all__ = [
|
||||||
|
"primitives",
|
||||||
|
"registry",
|
||||||
|
"adapters",
|
||||||
|
"proofs",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
from typing import Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from .primitives import LocalProblem, PlanDelta
|
||||||
|
from .proofs import generate_proof, generate_ec_keypair, ToyProof
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterBase:
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.name = name
|
||||||
|
# each adapter owns a keypair for signing proofs in this MVP
|
||||||
|
self._priv = generate_ec_keypair()
|
||||||
|
|
||||||
|
def private_key(self):
|
||||||
|
return self._priv
|
||||||
|
|
||||||
|
|
||||||
|
class SubstationMeterAdapter(AdapterBase):
|
||||||
|
"""Maps a substation meter reading to a LocalProblem."""
|
||||||
|
|
||||||
|
def to_local_problem(self, meter_payload: Dict[str, Any]) -> LocalProblem:
|
||||||
|
# Expect payload with node_id, timestamp, loads list, capacity
|
||||||
|
return LocalProblem(
|
||||||
|
schema_version="localproblem-v1",
|
||||||
|
node_id=meter_payload["node_id"],
|
||||||
|
timestamp=datetime.fromisoformat(meter_payload["timestamp"]),
|
||||||
|
horizon_minutes=int(meter_payload.get("horizon_minutes", 60)),
|
||||||
|
net_load_kw=meter_payload["net_load_kw"],
|
||||||
|
capacity_kw=float(meter_payload["capacity_kw"]),
|
||||||
|
metadata=meter_payload.get("metadata"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def produce_plan_and_proof(self, meter_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
lp = self.to_local_problem(meter_payload)
|
||||||
|
|
||||||
|
# naive deterministic plan: zero-out anything above capacity by scaling
|
||||||
|
avg = sum(lp.net_load_kw) / max(1, len(lp.net_load_kw))
|
||||||
|
scale = min(1.0, lp.capacity_kw / max(1e-6, avg))
|
||||||
|
plan = [v * scale for v in lp.net_load_kw]
|
||||||
|
|
||||||
|
delta = [p - v for p, v in zip(plan, lp.net_load_kw)]
|
||||||
|
|
||||||
|
pd = PlanDelta(schema_version="plandelta-v1", node_id=lp.node_id, delta_schedule_kw=delta, reason="scale_to_capacity")
|
||||||
|
|
||||||
|
# Create claims (toy): capacity respected if max(plan) <= capacity
|
||||||
|
claims = {"capacity_ok": max(plan) <= lp.capacity_kw}
|
||||||
|
|
||||||
|
proof = generate_proof(lp.dict(), claims, self._priv)
|
||||||
|
|
||||||
|
return {"plan_delta": pd, "proof": proof}
|
||||||
|
|
||||||
|
|
||||||
|
class DerAggregatorAdapter(AdapterBase):
|
||||||
|
"""Maps an aggregator's API to LocalProblem; slightly different heuristic."""
|
||||||
|
|
||||||
|
def to_local_problem(self, agg_payload: Dict[str, Any]) -> LocalProblem:
|
||||||
|
return LocalProblem(
|
||||||
|
schema_version="localproblem-v1",
|
||||||
|
node_id=agg_payload["id"],
|
||||||
|
timestamp=datetime.fromisoformat(agg_payload.get("ts")),
|
||||||
|
horizon_minutes=int(agg_payload.get("horizon", 60)),
|
||||||
|
net_load_kw=agg_payload["profile"],
|
||||||
|
capacity_kw=float(agg_payload.get("cap", 0.0)),
|
||||||
|
metadata=agg_payload.get("meta"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def produce_plan_and_proof(self, agg_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
lp = self.to_local_problem(agg_payload)
|
||||||
|
|
||||||
|
# simple heuristic: shift down high peaks by fixed margin 10%
|
||||||
|
plan = [v * 0.9 if v > 0.8 * lp.capacity_kw else v for v in lp.net_load_kw]
|
||||||
|
delta = [p - v for p, v in zip(plan, lp.net_load_kw)]
|
||||||
|
|
||||||
|
pd = PlanDelta(schema_version="plandelta-v1", node_id=lp.node_id, delta_schedule_kw=delta, reason="peak_shave_10pct")
|
||||||
|
|
||||||
|
claims = {"capacity_ok": max(plan) <= lp.capacity_kw}
|
||||||
|
|
||||||
|
proof = generate_proof(lp.dict(), claims, self._priv)
|
||||||
|
|
||||||
|
return {"plan_delta": pd, "proof": proof}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedModel(BaseModel):
|
||||||
|
schema_version: str = Field(..., description="Schema version identifier")
|
||||||
|
|
||||||
|
|
||||||
|
class LocalProblem(VersionedModel):
|
||||||
|
"""Canonical representation of a local optimization problem for a DER domain."""
|
||||||
|
node_id: str
|
||||||
|
timestamp: datetime
|
||||||
|
horizon_minutes: int
|
||||||
|
net_load_kw: List[float]
|
||||||
|
capacity_kw: float
|
||||||
|
metadata: Optional[Dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SharedVariables(VersionedModel):
|
||||||
|
"""Variables shared across domains (morphisms) for coordinated optimization."""
|
||||||
|
node_id: str
|
||||||
|
schedule_kw: List[float]
|
||||||
|
price_signal: Optional[List[float]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PlanDelta(VersionedModel):
|
||||||
|
"""Incremental plan updates produced by a local solver or adapter."""
|
||||||
|
node_id: str
|
||||||
|
delta_schedule_kw: List[float]
|
||||||
|
reason: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLog(VersionedModel):
|
||||||
|
"""Audit entry tying objects and proofs together for long-term verifiability."""
|
||||||
|
entry_id: str
|
||||||
|
created_at: datetime
|
||||||
|
author: str
|
||||||
|
object_hash: str
|
||||||
|
proof_ref: Optional[str] = None
|
||||||
|
metadata: Optional[Dict] = None
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import json
|
||||||
|
from hashlib import sha256
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature, decode_dss_signature
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(obj) -> bytes:
|
||||||
|
# Use json.dumps with a default serializer for datetimes and other non-serializable
|
||||||
|
# types. Keep keys sorted for deterministic commitments.
|
||||||
|
return json.dumps(
|
||||||
|
obj,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
default=lambda o: o.isoformat() if hasattr(o, "isoformat") else str(o),
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class ToyProof:
|
||||||
|
"""Toy proof: commitment (sha256 of object) + ECDSA signature by prover key.
|
||||||
|
|
||||||
|
The proof object carries a minimal claim set (e.g., capacity_ok) and a signature
|
||||||
|
over (commitment || claims) so verifiers can check the attestation without raw data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, commitment_hex: str, claims: dict, signature: bytes, pubkey_bytes: bytes):
|
||||||
|
self.commitment = commitment_hex
|
||||||
|
self.claims = claims
|
||||||
|
self.signature = signature
|
||||||
|
self.pubkey = pubkey_bytes
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"commitment": self.commitment,
|
||||||
|
"claims": self.claims,
|
||||||
|
"signature": self.signature.hex(),
|
||||||
|
"pubkey": self.pubkey.hex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_proof(obj: dict, claims: dict, private_key: ec.EllipticCurvePrivateKey) -> ToyProof:
|
||||||
|
# Commitment: hash of canonical JSON of the object
|
||||||
|
commitment = sha256(_canonical_json(obj)).hexdigest()
|
||||||
|
|
||||||
|
payload = {"commitment": commitment, "claims": claims}
|
||||||
|
payload_b = _canonical_json(payload)
|
||||||
|
|
||||||
|
signature = private_key.sign(payload_b, ec.ECDSA(hashes.SHA256()))
|
||||||
|
|
||||||
|
pubkey = private_key.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ToyProof(commitment, claims, signature, pubkey)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_proof(obj: dict, proof: ToyProof) -> bool:
|
||||||
|
# Recompute commitment
|
||||||
|
commitment = sha256(_canonical_json(obj)).hexdigest()
|
||||||
|
if commitment != proof.commitment:
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = {"commitment": proof.commitment, "claims": proof.claims}
|
||||||
|
payload_b = _canonical_json(payload)
|
||||||
|
|
||||||
|
# load pubkey
|
||||||
|
pubkey = serialization.load_der_public_key(proof.pubkey)
|
||||||
|
try:
|
||||||
|
pubkey.verify(proof.signature, payload_b, ec.ECDSA(hashes.SHA256()))
|
||||||
|
return True
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ec_keypair() -> ec.EllipticCurvePrivateKey:
|
||||||
|
return ec.generate_private_key(ec.SECP256R1())
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
class GraphOfContracts:
|
||||||
|
"""Simple in-memory registry mapping contract names to versioned schemas and adapters.
|
||||||
|
|
||||||
|
This is intentionally lightweight: it stores schema metadata and adapter references.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# structure: { contract_name: { version: {"schema": ..., "adapters": [names] } } }
|
||||||
|
self._store = {}
|
||||||
|
|
||||||
|
def register_schema(self, contract_name: str, version: str, schema: Any):
|
||||||
|
self._store.setdefault(contract_name, {})
|
||||||
|
self._store[contract_name][version] = {"schema": deepcopy(schema), "adapters": []}
|
||||||
|
|
||||||
|
def list_versions(self, contract_name: str) -> List[str]:
|
||||||
|
return list(self._store.get(contract_name, {}).keys())
|
||||||
|
|
||||||
|
def add_adapter(self, contract_name: str, version: str, adapter_name: str):
|
||||||
|
if contract_name not in self._store or version not in self._store[contract_name]:
|
||||||
|
raise KeyError("Schema version not found")
|
||||||
|
self._store[contract_name][version]["adapters"].append(adapter_name)
|
||||||
|
|
||||||
|
def get_schema(self, contract_name: str, version: str):
|
||||||
|
return self._store[contract_name][version]["schema"]
|
||||||
|
|
||||||
|
def adapters_for(self, contract_name: str, version: str):
|
||||||
|
return list(self._store[contract_name][version]["adapters"])
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
||||||
|
echo "Installing package in editable mode..."
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
echo "Running pytest..."
|
||||||
|
pytest -q
|
||||||
|
|
||||||
|
echo "Building package (python3 -m build)..."
|
||||||
|
python3 -m build
|
||||||
|
|
||||||
|
echo "All tests and build completed successfully."
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import pytest
|
||||||
|
from idea85_blockenergy_mesh_verifiable.primitives import LocalProblem
|
||||||
|
from idea85_blockenergy_mesh_verifiable.adapters import SubstationMeterAdapter, DerAggregatorAdapter
|
||||||
|
from idea85_blockenergy_mesh_verifiable.proofs import verify_proof
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def sample_meter_payload():
|
||||||
|
return {
|
||||||
|
"node_id": "substation-1",
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"horizon_minutes": 60,
|
||||||
|
"net_load_kw": [10.0, 12.0, 11.0, 9.0],
|
||||||
|
"capacity_kw": 12.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_substation_adapter_plan_and_proof():
|
||||||
|
adapter = SubstationMeterAdapter("submeter")
|
||||||
|
payload = sample_meter_payload()
|
||||||
|
out = adapter.produce_plan_and_proof(payload)
|
||||||
|
|
||||||
|
assert "plan_delta" in out and "proof" in out
|
||||||
|
plan_delta = out["plan_delta"]
|
||||||
|
proof = out["proof"]
|
||||||
|
|
||||||
|
# Plan delta is a PlanDelta model
|
||||||
|
assert plan_delta.node_id == payload["node_id"]
|
||||||
|
# Proof verifies against original LocalProblem
|
||||||
|
lp = adapter.to_local_problem(payload)
|
||||||
|
assert verify_proof(lp.dict(), proof)
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_adapter_plan_and_proof():
|
||||||
|
adapter = DerAggregatorAdapter("deragg")
|
||||||
|
payload = {
|
||||||
|
"id": "der-1",
|
||||||
|
"ts": datetime.utcnow().isoformat(),
|
||||||
|
"horizon": 60,
|
||||||
|
"profile": [2.0, 3.0, 5.0, 1.0],
|
||||||
|
"cap": 6.0,
|
||||||
|
}
|
||||||
|
out = adapter.produce_plan_and_proof(payload)
|
||||||
|
assert "plan_delta" in out and "proof" in out
|
||||||
|
lp = adapter.to_local_problem(payload)
|
||||||
|
assert verify_proof(lp.dict(), out["proof"]) is True
|
||||||
Loading…
Reference in New Issue