Store provenance on IPFS instead of local files
- Add add_json() to ipfs_client for storing JSON data - Update render_effect task to store provenance on IPFS - Update execute_dag task to store DAG provenance on IPFS - Add provenance_cid field to RunStatus model - Extract provenance_cid from task results Provenance is now immutable and content-addressed, enabling: - Cross-L2 verification - Bitcoin timestamping for dispute resolution - Complete audit trail on IPFS Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,22 @@ def add_bytes(data: bytes, pin: bool = True) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_json(data: dict, pin: bool = True) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Serialize dict to JSON and add to IPFS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Dictionary to serialize and store
|
||||||
|
pin: Whether to pin the data (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
IPFS CID or None on failure
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
json_bytes = json.dumps(data, indent=2, sort_keys=True).encode('utf-8')
|
||||||
|
return add_bytes(json_bytes, pin=pin)
|
||||||
|
|
||||||
|
|
||||||
def get_file(cid: str, dest_path: Path) -> bool:
|
def get_file(cid: str, dest_path: Path) -> bool:
|
||||||
"""
|
"""
|
||||||
Retrieve a file from IPFS and save to destination.
|
Retrieve a file from IPFS and save to destination.
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class RunStatus(BaseModel):
|
|||||||
effect_url: Optional[str] = None # URL to effect source code
|
effect_url: Optional[str] = None # URL to effect source code
|
||||||
username: Optional[str] = None # Owner of the run (ActivityPub actor ID)
|
username: Optional[str] = None # Owner of the run (ActivityPub actor ID)
|
||||||
infrastructure: Optional[dict] = None # Hardware/software used for rendering
|
infrastructure: Optional[dict] = None # Hardware/software used for rendering
|
||||||
|
provenance_cid: Optional[str] = None # IPFS CID of provenance record
|
||||||
|
|
||||||
|
|
||||||
# ============ Recipe Models ============
|
# ============ Recipe Models ============
|
||||||
@@ -616,10 +617,12 @@ async def get_run(run_id: str):
|
|||||||
if "output_hash" in result:
|
if "output_hash" in result:
|
||||||
# New DAG result format
|
# New DAG result format
|
||||||
run.output_hash = result.get("output_hash")
|
run.output_hash = result.get("output_hash")
|
||||||
|
run.provenance_cid = result.get("provenance_cid")
|
||||||
output_path = Path(result.get("output_path", "")) if result.get("output_path") else None
|
output_path = Path(result.get("output_path", "")) if result.get("output_path") else None
|
||||||
else:
|
else:
|
||||||
# Legacy render_effect format
|
# Legacy render_effect format
|
||||||
run.output_hash = result.get("output", {}).get("content_hash")
|
run.output_hash = result.get("output", {}).get("content_hash")
|
||||||
|
run.provenance_cid = result.get("provenance_cid")
|
||||||
output_path = Path(result.get("output", {}).get("local_path", ""))
|
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||||
|
|
||||||
# Extract effects info from provenance (legacy only)
|
# Extract effects info from provenance (legacy only)
|
||||||
|
|||||||
47
tasks.py
47
tasks.py
@@ -237,7 +237,6 @@ def render_effect(self, input_hash: str, effect_name: str, output_name: str) ->
|
|||||||
"output": {
|
"output": {
|
||||||
"name": output_name,
|
"name": output_name,
|
||||||
"content_hash": output_hash,
|
"content_hash": output_hash,
|
||||||
"local_path": str(result)
|
|
||||||
},
|
},
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{"content_hash": input_hash}
|
{"content_hash": input_hash}
|
||||||
@@ -249,10 +248,14 @@ def render_effect(self, input_hash: str, effect_name: str, output_name: str) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save provenance
|
# Store provenance on IPFS
|
||||||
provenance_path = result.with_suffix(".provenance.json")
|
import ipfs_client
|
||||||
with open(provenance_path, "w") as f:
|
provenance_cid = ipfs_client.add_json(provenance)
|
||||||
json.dump(provenance, f, indent=2)
|
if provenance_cid:
|
||||||
|
provenance["provenance_cid"] = provenance_cid
|
||||||
|
logger.info(f"Stored provenance on IPFS: {provenance_cid}")
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to store provenance on IPFS")
|
||||||
|
|
||||||
return provenance
|
return provenance
|
||||||
|
|
||||||
@@ -339,6 +342,39 @@ def execute_dag(self, dag_json: str, run_id: str = None) -> dict:
|
|||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build provenance
|
||||||
|
input_hashes_for_provenance = []
|
||||||
|
for node_id, node in dag.nodes.items():
|
||||||
|
if node.node_type == NodeType.SOURCE or str(node.node_type) == "SOURCE":
|
||||||
|
content_hash = node.config.get("content_hash")
|
||||||
|
if content_hash:
|
||||||
|
input_hashes_for_provenance.append({"content_hash": content_hash})
|
||||||
|
|
||||||
|
provenance = {
|
||||||
|
"task_id": self.request.id,
|
||||||
|
"run_id": run_id,
|
||||||
|
"rendered_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"output": {
|
||||||
|
"content_hash": output_hash,
|
||||||
|
},
|
||||||
|
"inputs": input_hashes_for_provenance,
|
||||||
|
"dag": dag_json, # Full DAG definition
|
||||||
|
"execution": {
|
||||||
|
"execution_time": result.execution_time,
|
||||||
|
"nodes_executed": result.nodes_executed,
|
||||||
|
"nodes_cached": result.nodes_cached,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store provenance on IPFS
|
||||||
|
import ipfs_client
|
||||||
|
provenance_cid = ipfs_client.add_json(provenance)
|
||||||
|
if provenance_cid:
|
||||||
|
provenance["provenance_cid"] = provenance_cid
|
||||||
|
logger.info(f"Stored DAG provenance on IPFS: {provenance_cid}")
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to store DAG provenance on IPFS")
|
||||||
|
|
||||||
# Build result
|
# Build result
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -351,6 +387,7 @@ def execute_dag(self, dag_json: str, run_id: str = None) -> dict:
|
|||||||
"node_results": {
|
"node_results": {
|
||||||
node_id: str(path) for node_id, path in result.node_results.items()
|
node_id: str(path) for node_id, path in result.node_results.items()
|
||||||
},
|
},
|
||||||
|
"provenance_cid": provenance_cid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user