core-runtime-engine 11.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. core_runtime/__init__.py +10 -0
  2. core_runtime/__version__.py +17 -0
  3. core_runtime/cli/__init__.py +7 -0
  4. core_runtime/cli/__main__.py +22 -0
  5. core_runtime/cli/bump_version.py +176 -0
  6. core_runtime/cli/contract_preflight.py +69 -0
  7. core_runtime/cli/create_domain.py +66 -0
  8. core_runtime/cli/doctor.py +44 -0
  9. core_runtime/cli/inventory.py +85 -0
  10. core_runtime/cli/lint.py +8 -0
  11. core_runtime/cli/main.py +579 -0
  12. core_runtime/cli/release_check.py +65 -0
  13. core_runtime/cli/repair_artifact_paths.py +48 -0
  14. core_runtime/cli/sync_template.py +67 -0
  15. core_runtime/cli/validate.py +73 -0
  16. core_runtime/core/__init__.py +34 -0
  17. core_runtime/core/audit_event.py +760 -0
  18. core_runtime/core/audit_trail_index.py +100 -0
  19. core_runtime/core/canonicalization.py +55 -0
  20. core_runtime/core/contract_evaluator.py +945 -0
  21. core_runtime/core/contract_executability.py +307 -0
  22. core_runtime/core/contract_loader.py +57 -0
  23. core_runtime/core/contract_probes.py +630 -0
  24. core_runtime/core/contract_program.py +131 -0
  25. core_runtime/core/contract_program_registry.py +104 -0
  26. core_runtime/core/contract_program_v2.py +126 -0
  27. core_runtime/core/dsk_v3.py +142 -0
  28. core_runtime/core/explainability.py +2115 -0
  29. core_runtime/core/numeric_normalization.py +120 -0
  30. core_runtime/core/rule_anchor.py +1388 -0
  31. core_runtime/core/schema_fingerprint.py +69 -0
  32. core_runtime/core/sensor_evidence.py +548 -0
  33. core_runtime/data/contracts/CoreAnchor.sol +109 -0
  34. core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
  35. core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
  36. core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
  37. core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
  38. core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
  39. core_runtime/data/package_data_manifest.v1.json +173 -0
  40. core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
  41. core_runtime/data/schemas/core/context_gate.v1.json +38 -0
  42. core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
  43. core_runtime/data/schemas/core/contract_program.v1.json +186 -0
  44. core_runtime/data/schemas/core/contract_program.v2.json +187 -0
  45. core_runtime/data/schemas/core/control_decision.v1.json +114 -0
  46. core_runtime/data/schemas/core/dsk.v3.json +105 -0
  47. core_runtime/data/schemas/core/effect_result.v1.json +39 -0
  48. core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
  49. core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
  50. core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
  51. core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
  52. core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
  53. core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
  54. core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
  55. core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
  56. core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
  57. core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
  58. core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
  59. core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
  60. core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
  61. core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
  62. core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
  63. core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
  64. core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
  65. core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
  66. core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
  67. core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
  68. core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
  69. core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
  70. core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
  71. core_runtime/data/schemas/core/state_transition.v1.json +115 -0
  72. core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
  73. core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
  74. core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
  75. core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
  76. core_runtime/tooling/__init__.py +48 -0
  77. core_runtime/tooling/bump_version.py +900 -0
  78. core_runtime/tooling/contract_preflight.py +293 -0
  79. core_runtime/tooling/create_domain.py +254 -0
  80. core_runtime/tooling/diagnostics.py +129 -0
  81. core_runtime/tooling/doctor.py +482 -0
  82. core_runtime/tooling/file_inventory.py +182 -0
  83. core_runtime/tooling/json_checks.py +100 -0
  84. core_runtime/tooling/release_check.py +1017 -0
  85. core_runtime/tooling/repair_artifact_paths.py +458 -0
  86. core_runtime/tooling/report_writer.py +176 -0
  87. core_runtime/tooling/repository_inventory.py +399 -0
  88. core_runtime/tooling/safety_checks.py +172 -0
  89. core_runtime/tooling/sync_template.py +303 -0
  90. core_runtime/tooling/validation.py +507 -0
  91. core_runtime/tooling/version_inventory.py +256 -0
  92. core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
  93. core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
  94. core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
  95. core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
  96. core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1388 @@
1
+ """Deterministic primitives for frozen-rule approval and blockchain anchoring.
2
+
3
+ CORE validates rule artifacts and signatures off-chain. The blockchain
4
+ contract only timestamps a Merkle root and its manifest fingerprint. No
5
+ private rule content, blinding nonce, wallet secret, or runtime authority is
6
+ placed on-chain.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import re
14
+ from collections.abc import Iterable, Mapping, Sequence
15
+ from datetime import datetime
16
+ from importlib.resources import files
17
+ from typing import Any
18
+
19
+ from jsonschema import Draft7Validator
20
+
21
+ from core_runtime.core.canonicalization import canonical_json_dumps
22
+
23
+
24
+ SCHEMA_ROOT = files("core_runtime").joinpath("data", "schemas", "core")
25
+ CONTRACT_ROOT = files("core_runtime").joinpath("data", "contracts")
26
+
27
+ FROZEN_RULE_SET_SCHEMA = "core.frozen_rule_set.v1"
28
+ APPROVAL_REQUEST_SCHEMA = "core.rule_approval_request.v1"
29
+ APPROVAL_SCHEMA = "core.rule_approval.v1"
30
+ ANCHOR_BATCH_SCHEMA = "core.rule_anchor_batch.v1"
31
+ UNSIGNED_TRANSACTION_SCHEMA = "core.unsigned_rule_anchor_transaction.v1"
32
+ UNSIGNED_DEPLOYMENT_SCHEMA = "core.unsigned_rule_anchor_deployment.v1"
33
+ CHAIN_EVIDENCE_SCHEMA = "core.rule_anchor_chain_evidence.v1"
34
+
35
+ FINGERPRINT_RE = re.compile(r"^sha256:[a-f0-9]{64}$")
36
+ ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
37
+ SIGNATURE_RE = re.compile(r"^0x[a-fA-F0-9]{130}$")
38
+
39
+ PRIVATE_COMMITMENT_DOMAIN = b"CORE_PRIVATE_RULE_COMMITMENT_V1\x00"
40
+ MERKLE_LEAF_DOMAIN = b"\x00CORE_RULE_ANCHOR_LEAF_V1\x00"
41
+ MERKLE_NODE_DOMAIN = b"\x01CORE_RULE_ANCHOR_NODE_V1\x00"
42
+ APPROVAL_MESSAGE_HEADER = "CORE FrozenRuleSet Approval v1"
43
+
44
+ # Keccak-256("anchorRuleBatch(bytes32,bytes32,uint32,uint8)")[:4].
45
+ # The full signature and selector are independently checked in tests/CI.
46
+ ANCHOR_RULE_BATCH_SELECTOR = "6919e458"
47
+
48
+ # secp256k1 group order, used to reject malleable high-s signatures.
49
+ SECP256K1_N = int(
50
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",
51
+ 16,
52
+ )
53
+
54
+ FORBIDDEN_WALLET_SECRET_KEYS = frozenset(
55
+ {
56
+ "private_key",
57
+ "private-key",
58
+ "seed",
59
+ "seed_phrase",
60
+ "mnemonic",
61
+ "password",
62
+ "signing_key",
63
+ "raw_transaction",
64
+ }
65
+ )
66
+ UNSIGNED_DEPLOYMENT_WARNINGS = (
67
+ "Review and sign this deployment only in an external wallet.",
68
+ "After confirmation, verify deployed runtime bytecode before creating approvals.",
69
+ "CORE never requests or stores wallet secrets and never broadcasts.",
70
+ )
71
+
72
+
73
+ def error(code: str, message: str, field: str | None = None, **extra: Any) -> dict[str, Any]:
74
+ """Return a stable validator error envelope entry."""
75
+
76
+ result: dict[str, Any] = {"code": code, "message": message}
77
+ if field is not None:
78
+ result["field"] = field
79
+ result.update(extra)
80
+ return result
81
+
82
+
83
+ def sha256_fingerprint_bytes(payload: bytes) -> str:
84
+ """Return a prefixed SHA-256 fingerprint."""
85
+
86
+ return f"sha256:{hashlib.sha256(payload).hexdigest()}"
87
+
88
+
89
+ def canonical_fingerprint(payload: Any) -> str:
90
+ """Fingerprint canonical UTF-8 JSON using SHA-256."""
91
+
92
+ return sha256_fingerprint_bytes(canonical_json_dumps(payload).encode("utf-8"))
93
+
94
+
95
+ def artifact_fingerprint(payload: Mapping[str, Any], field: str = "fingerprint") -> str:
96
+ """Fingerprint an artifact while excluding its declared fingerprint."""
97
+
98
+ return canonical_fingerprint({key: value for key, value in payload.items() if key != field})
99
+
100
+
101
+ def load_verified_rule_anchor_build() -> tuple[dict[str, Any], str]:
102
+ """Load frozen contract artifacts and verify every declared build digest."""
103
+
104
+ build = json.loads((CONTRACT_ROOT / "CoreRuleAnchor.build.json").read_text(encoding="utf-8"))
105
+ source = (CONTRACT_ROOT / "CoreRuleAnchor.sol").read_bytes()
106
+ abi = json.loads((CONTRACT_ROOT / "CoreRuleAnchor.abi.json").read_text(encoding="utf-8"))
107
+ creation_hex = (CONTRACT_ROOT / "CoreRuleAnchor.bin").read_text(encoding="utf-8").strip()
108
+ runtime_hex = (CONTRACT_ROOT / "CoreRuleAnchor.runtime.bin").read_text(encoding="utf-8").strip()
109
+ checks = {
110
+ "source_sha256": sha256_fingerprint_bytes(source),
111
+ "abi_canonical_sha256": canonical_fingerprint(abi),
112
+ "creation_bytecode_sha256": sha256_fingerprint_bytes(bytes.fromhex(creation_hex)),
113
+ "runtime_bytecode_sha256": sha256_fingerprint_bytes(bytes.fromhex(runtime_hex)),
114
+ }
115
+ for field, computed in checks.items():
116
+ if build.get(field) != computed:
117
+ raise ValueError(f"frozen contract build mismatch: {field}")
118
+ return build, creation_hex
119
+
120
+
121
+ def _wallet_secret_errors(payload: Any, artifact_label: str) -> list[dict[str, Any]]:
122
+ errors: list[dict[str, Any]] = []
123
+
124
+ def scan(value: Any, field: str = "$") -> None:
125
+ if isinstance(value, dict):
126
+ for key, child in value.items():
127
+ child_field = f"{field}.{key}"
128
+ if str(key).lower() in FORBIDDEN_WALLET_SECRET_KEYS:
129
+ errors.append(
130
+ error(
131
+ "wallet_secret_forbidden",
132
+ f"{artifact_label} artifacts cannot contain wallet secrets.",
133
+ child_field,
134
+ )
135
+ )
136
+ scan(child, child_field)
137
+ elif isinstance(value, list):
138
+ for index, child in enumerate(value):
139
+ scan(child, f"{field}[{index}]")
140
+
141
+ scan(payload)
142
+ return errors
143
+
144
+
145
+ def fingerprint_bytes(value: str) -> bytes:
146
+ """Decode a canonical ``sha256:<hex>`` fingerprint."""
147
+
148
+ if not FINGERPRINT_RE.fullmatch(value):
149
+ raise ValueError("fingerprint must match sha256:<64 lowercase hex characters>")
150
+ return bytes.fromhex(value.removeprefix("sha256:"))
151
+
152
+
153
+ def bytes32_hex(value: str) -> str:
154
+ """Convert a SHA-256 fingerprint to an EVM bytes32 hex value."""
155
+
156
+ return "0x" + fingerprint_bytes(value).hex()
157
+
158
+
159
+ def _schema_errors(payload: Any, schema_filename: str) -> list[dict[str, Any]]:
160
+ schema_path = SCHEMA_ROOT / schema_filename
161
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
162
+ validator = Draft7Validator(schema)
163
+ errors: list[dict[str, Any]] = []
164
+ for item in sorted(validator.iter_errors(payload), key=lambda entry: list(entry.absolute_path)):
165
+ field = ".".join(str(part) for part in item.absolute_path) or "$"
166
+ errors.append(error("schema_validation_error", item.message, field))
167
+ return errors
168
+
169
+
170
+ def _valid_timezone_timestamp(value: Any) -> bool:
171
+ if not isinstance(value, str):
172
+ return False
173
+ normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
174
+ try:
175
+ parsed = datetime.fromisoformat(normalized)
176
+ except ValueError:
177
+ return False
178
+ return parsed.tzinfo is not None and parsed.utcoffset() is not None
179
+
180
+
181
+ def _normalized_addresses(values: Iterable[str]) -> list[str]:
182
+ return [value.lower() for value in values]
183
+
184
+
185
+ def validate_frozen_rule_set_payload(payload: Any) -> list[dict[str, Any]]:
186
+ """Validate a public frozen rule set or a private commitment envelope."""
187
+
188
+ errors = _schema_errors(payload, "frozen_rule_set.v1.json")
189
+ if not isinstance(payload, dict):
190
+ return errors
191
+
192
+ frozen_at = payload.get("frozen_at")
193
+ if frozen_at is not None and not _valid_timezone_timestamp(frozen_at):
194
+ errors.append(
195
+ error(
196
+ "invalid_frozen_at",
197
+ "frozen_at must be an ISO 8601 timestamp with an explicit timezone.",
198
+ "frozen_at",
199
+ )
200
+ )
201
+
202
+ governance = payload.get("governance")
203
+ if isinstance(governance, dict):
204
+ signers = governance.get("authorized_signers")
205
+ threshold = governance.get("approval_threshold")
206
+ if isinstance(signers, list) and all(isinstance(item, str) for item in signers):
207
+ normalized = _normalized_addresses(signers)
208
+ if len(set(normalized)) != len(normalized):
209
+ errors.append(
210
+ error(
211
+ "duplicate_authorized_signer",
212
+ "authorized_signers must be unique ignoring address case.",
213
+ "governance.authorized_signers",
214
+ )
215
+ )
216
+ if isinstance(threshold, int) and not isinstance(threshold, bool) and threshold > len(signers):
217
+ errors.append(
218
+ error(
219
+ "approval_threshold_unreachable",
220
+ "approval_threshold cannot exceed the number of authorized signers.",
221
+ "governance.approval_threshold",
222
+ )
223
+ )
224
+
225
+ content = payload.get("content")
226
+ rule_class = payload.get("rule_class")
227
+ visibility = payload.get("visibility")
228
+ if isinstance(content, dict):
229
+ mode = content.get("mode")
230
+ if rule_class == "general" and (visibility != "public" or mode != "public"):
231
+ errors.append(
232
+ error(
233
+ "general_rule_must_be_public",
234
+ "General rules must publish their complete frozen content.",
235
+ "visibility",
236
+ )
237
+ )
238
+ if rule_class == "personal" and (
239
+ visibility != "private_commitment" or mode != "private_commitment"
240
+ ):
241
+ errors.append(
242
+ error(
243
+ "personal_rule_must_be_private_commitment",
244
+ "Personal rules must expose only a blinded commitment.",
245
+ "visibility",
246
+ )
247
+ )
248
+
249
+ if mode == "public":
250
+ rules = content.get("rules")
251
+ if isinstance(rules, list):
252
+ rule_ids = [item.get("rule_id") for item in rules if isinstance(item, dict)]
253
+ if len(rule_ids) != len(set(rule_ids)):
254
+ errors.append(
255
+ error(
256
+ "duplicate_rule_id",
257
+ "Public rule_id values must be unique within a frozen rule set.",
258
+ "content.rules",
259
+ )
260
+ )
261
+ for index, rule in enumerate(rules):
262
+ if not isinstance(rule, dict):
263
+ continue
264
+ if rule.get("domain") != payload.get("domain"):
265
+ errors.append(
266
+ error(
267
+ "rule_domain_mismatch",
268
+ "Every public rule domain must equal the rule-set domain.",
269
+ f"content.rules.{index}.domain",
270
+ )
271
+ )
272
+ steps = rule.get("steps")
273
+ if isinstance(steps, list):
274
+ step_ids = [step.get("step_id") for step in steps if isinstance(step, dict)]
275
+ if len(step_ids) != len(set(step_ids)):
276
+ errors.append(
277
+ error(
278
+ "duplicate_step_id",
279
+ "step_id values must be unique within each rule.",
280
+ f"content.rules.{index}.steps",
281
+ )
282
+ )
283
+
284
+ if mode == "private_commitment":
285
+ commitment = content.get("commitment")
286
+ if commitment == "sha256:" + ("0" * 64):
287
+ errors.append(
288
+ error(
289
+ "zero_private_commitment",
290
+ "A private commitment cannot be the all-zero digest.",
291
+ "content.commitment",
292
+ )
293
+ )
294
+
295
+ declared = payload.get("fingerprint")
296
+ if isinstance(declared, str) and FINGERPRINT_RE.fullmatch(declared):
297
+ expected = artifact_fingerprint(payload)
298
+ if declared != expected:
299
+ errors.append(
300
+ error(
301
+ "fingerprint_mismatch",
302
+ "Frozen rule-set fingerprint does not match canonical content.",
303
+ "fingerprint",
304
+ declared=declared,
305
+ computed=expected,
306
+ )
307
+ )
308
+
309
+ return errors
310
+
311
+
312
+ def private_content_fingerprint(private_payload: Any) -> str:
313
+ """Fingerprint private content without publishing that digest."""
314
+
315
+ return canonical_fingerprint(private_payload)
316
+
317
+
318
+ def private_rule_commitment(private_payload: Any, blinding_nonce: bytes) -> str:
319
+ """Create a domain-separated commitment hiding low-entropy private rules."""
320
+
321
+ if len(blinding_nonce) != 32:
322
+ raise ValueError("blinding_nonce must contain exactly 32 bytes")
323
+ content_digest = fingerprint_bytes(private_content_fingerprint(private_payload))
324
+ return sha256_fingerprint_bytes(PRIVATE_COMMITMENT_DOMAIN + content_digest + blinding_nonce)
325
+
326
+
327
+ def verify_private_rule_opening(
328
+ private_payload: Any,
329
+ blinding_nonce_hex: str,
330
+ expected_commitment: str,
331
+ ) -> bool:
332
+ """Verify a private commitment opening locally."""
333
+
334
+ if not re.fullmatch(r"[a-f0-9]{64}", blinding_nonce_hex):
335
+ return False
336
+ if not FINGERPRINT_RE.fullmatch(expected_commitment):
337
+ return False
338
+ return private_rule_commitment(private_payload, bytes.fromhex(blinding_nonce_hex)) == expected_commitment
339
+
340
+
341
+ def approval_message(rule_set_fingerprint: str, chain_id: int, verifying_contract: str) -> str:
342
+ """Build the exact human-readable EIP-191 approval message."""
343
+
344
+ fingerprint_bytes(rule_set_fingerprint)
345
+ if not isinstance(chain_id, int) or isinstance(chain_id, bool) or chain_id <= 0:
346
+ raise ValueError("chain_id must be a positive integer")
347
+ if not ADDRESS_RE.fullmatch(verifying_contract):
348
+ raise ValueError("verifying_contract must be an EVM address")
349
+ return "\n".join(
350
+ (
351
+ APPROVAL_MESSAGE_HEADER,
352
+ f"rule_set_fingerprint: {rule_set_fingerprint}",
353
+ f"chain_id: {chain_id}",
354
+ f"verifying_contract: {verifying_contract.lower()}",
355
+ "decision: approve_frozen_rule_set",
356
+ )
357
+ )
358
+
359
+
360
+ def build_approval_request(
361
+ rule_set_fingerprint: str,
362
+ chain_id: int,
363
+ verifying_contract: str,
364
+ signer: str,
365
+ ) -> dict[str, Any]:
366
+ """Build an externally signable, domain-bound approval request."""
367
+
368
+ if not ADDRESS_RE.fullmatch(signer):
369
+ raise ValueError("signer must be an EVM address")
370
+ request: dict[str, Any] = {
371
+ "schema_version": APPROVAL_REQUEST_SCHEMA,
372
+ "type": "rule_approval_request",
373
+ "rule_set_fingerprint": rule_set_fingerprint,
374
+ "chain_id": chain_id,
375
+ "verifying_contract": verifying_contract.lower(),
376
+ "signer": signer.lower(),
377
+ "signature_scheme": "eip191_secp256k1",
378
+ "decision": "approve_frozen_rule_set",
379
+ "message": approval_message(rule_set_fingerprint, chain_id, verifying_contract),
380
+ }
381
+ request["fingerprint"] = artifact_fingerprint(request)
382
+ return request
383
+
384
+
385
+ def validate_approval_request_payload(payload: Any) -> list[dict[str, Any]]:
386
+ """Validate an unsigned approval request."""
387
+
388
+ errors = _schema_errors(payload, "rule_approval_request.v1.json")
389
+ if not isinstance(payload, dict):
390
+ return errors
391
+
392
+ try:
393
+ expected_message = approval_message(
394
+ payload.get("rule_set_fingerprint", ""),
395
+ payload.get("chain_id", 0),
396
+ payload.get("verifying_contract", ""),
397
+ )
398
+ except (TypeError, ValueError):
399
+ expected_message = None
400
+
401
+ if expected_message is not None and payload.get("message") != expected_message:
402
+ errors.append(
403
+ error(
404
+ "approval_message_mismatch",
405
+ "Approval message does not match its rule, chain, and contract binding.",
406
+ "message",
407
+ )
408
+ )
409
+
410
+ declared = payload.get("fingerprint")
411
+ if isinstance(declared, str) and FINGERPRINT_RE.fullmatch(declared):
412
+ expected = artifact_fingerprint(payload)
413
+ if declared != expected:
414
+ errors.append(
415
+ error(
416
+ "fingerprint_mismatch",
417
+ "Approval-request fingerprint does not match canonical content.",
418
+ "fingerprint",
419
+ declared=declared,
420
+ computed=expected,
421
+ )
422
+ )
423
+ return errors
424
+
425
+
426
+ def _validate_canonical_signature(signature: str) -> list[dict[str, Any]]:
427
+ if not SIGNATURE_RE.fullmatch(signature):
428
+ return [
429
+ error(
430
+ "invalid_signature_format",
431
+ "signature must be a 65-byte 0x-prefixed hexadecimal ECDSA signature.",
432
+ "signature",
433
+ )
434
+ ]
435
+ raw = bytes.fromhex(signature[2:])
436
+ r_value = int.from_bytes(raw[0:32], "big")
437
+ s_value = int.from_bytes(raw[32:64], "big")
438
+ recovery_id = raw[64]
439
+ errors: list[dict[str, Any]] = []
440
+ if r_value <= 0 or r_value >= SECP256K1_N:
441
+ errors.append(error("invalid_signature_r", "signature r is outside secp256k1 range.", "signature"))
442
+ if s_value <= 0 or s_value > SECP256K1_N // 2:
443
+ errors.append(
444
+ error(
445
+ "noncanonical_signature_s",
446
+ "signature must use canonical low-s form.",
447
+ "signature",
448
+ )
449
+ )
450
+ if recovery_id not in {0, 1, 27, 28}:
451
+ errors.append(
452
+ error(
453
+ "invalid_signature_recovery_id",
454
+ "signature recovery id must be 0, 1, 27, or 28.",
455
+ "signature",
456
+ )
457
+ )
458
+ return errors
459
+
460
+
461
+ def recover_eip191_signer(message: str, signature: str) -> str:
462
+ """Recover the signer address; fail closed when the crypto extra is absent."""
463
+
464
+ try:
465
+ from eth_account import Account
466
+ from eth_account.messages import encode_defunct
467
+ except ImportError as exc: # pragma: no cover - exercised in dependency-minimal installs
468
+ raise RuntimeError("anchoring crypto backend unavailable; install CORE's anchoring extra") from exc
469
+
470
+ recovered = Account.recover_message(encode_defunct(text=message), signature=signature)
471
+ return str(recovered).lower()
472
+
473
+
474
+ def validate_rule_approval_payload(payload: Any) -> list[dict[str, Any]]:
475
+ """Validate and cryptographically recover a frozen-rule approval."""
476
+
477
+ errors = _schema_errors(payload, "rule_approval.v1.json")
478
+ if not isinstance(payload, dict):
479
+ return errors
480
+
481
+ signature = payload.get("signature")
482
+ if isinstance(signature, str):
483
+ errors.extend(_validate_canonical_signature(signature))
484
+
485
+ try:
486
+ expected_message = approval_message(
487
+ payload.get("rule_set_fingerprint", ""),
488
+ payload.get("chain_id", 0),
489
+ payload.get("verifying_contract", ""),
490
+ )
491
+ except (TypeError, ValueError):
492
+ expected_message = None
493
+
494
+ if expected_message is not None and payload.get("message") != expected_message:
495
+ errors.append(
496
+ error(
497
+ "approval_message_mismatch",
498
+ "Approval message does not match its rule, chain, and contract binding.",
499
+ "message",
500
+ )
501
+ )
502
+
503
+ if expected_message is not None and isinstance(signature, str) and not _validate_canonical_signature(signature):
504
+ try:
505
+ recovered = recover_eip191_signer(expected_message, signature)
506
+ except RuntimeError as exc:
507
+ errors.append(error("signature_verification_failed", str(exc), "signature"))
508
+ except Exception:
509
+ errors.append(
510
+ error(
511
+ "signature_verification_failed",
512
+ "ECDSA recovery rejected the supplied signature.",
513
+ "signature",
514
+ )
515
+ )
516
+ else:
517
+ declared_signer = payload.get("signer")
518
+ if isinstance(declared_signer, str) and recovered != declared_signer.lower():
519
+ errors.append(
520
+ error(
521
+ "signature_signer_mismatch",
522
+ "Recovered signature address does not match signer.",
523
+ "signer",
524
+ recovered=recovered,
525
+ )
526
+ )
527
+
528
+ request_fingerprint = payload.get("approval_request_fingerprint")
529
+ if isinstance(request_fingerprint, str) and FINGERPRINT_RE.fullmatch(request_fingerprint):
530
+ request = {
531
+ "schema_version": APPROVAL_REQUEST_SCHEMA,
532
+ "type": "rule_approval_request",
533
+ "rule_set_fingerprint": payload.get("rule_set_fingerprint"),
534
+ "chain_id": payload.get("chain_id"),
535
+ "verifying_contract": payload.get("verifying_contract"),
536
+ "signer": payload.get("signer"),
537
+ "signature_scheme": payload.get("signature_scheme"),
538
+ "decision": payload.get("decision"),
539
+ "message": payload.get("message"),
540
+ }
541
+ expected_request_fingerprint = artifact_fingerprint(request)
542
+ if request_fingerprint != expected_request_fingerprint:
543
+ errors.append(
544
+ error(
545
+ "approval_request_fingerprint_mismatch",
546
+ "approval_request_fingerprint does not match the signed request.",
547
+ "approval_request_fingerprint",
548
+ declared=request_fingerprint,
549
+ computed=expected_request_fingerprint,
550
+ )
551
+ )
552
+
553
+ declared = payload.get("fingerprint")
554
+ if isinstance(declared, str) and FINGERPRINT_RE.fullmatch(declared):
555
+ expected = artifact_fingerprint(payload)
556
+ if declared != expected:
557
+ errors.append(
558
+ error(
559
+ "fingerprint_mismatch",
560
+ "Rule-approval fingerprint does not match canonical content.",
561
+ "fingerprint",
562
+ declared=declared,
563
+ computed=expected,
564
+ )
565
+ )
566
+ return errors
567
+
568
+
569
+ def finalize_approval_request(request: Mapping[str, Any], signature: str) -> dict[str, Any]:
570
+ """Attach an externally produced signature to a validated request."""
571
+
572
+ request_errors = validate_approval_request_payload(dict(request))
573
+ if request_errors:
574
+ raise ValueError(f"invalid approval request: {request_errors[0]['code']}")
575
+ approval: dict[str, Any] = {
576
+ "schema_version": APPROVAL_SCHEMA,
577
+ "type": "rule_approval",
578
+ "approval_request_fingerprint": request["fingerprint"],
579
+ "rule_set_fingerprint": request["rule_set_fingerprint"],
580
+ "chain_id": request["chain_id"],
581
+ "verifying_contract": request["verifying_contract"],
582
+ "signer": request["signer"],
583
+ "signature_scheme": request["signature_scheme"],
584
+ "decision": request["decision"],
585
+ "message": request["message"],
586
+ "signature": signature,
587
+ }
588
+ approval["fingerprint"] = artifact_fingerprint(approval)
589
+ approval_errors = validate_rule_approval_payload(approval)
590
+ if approval_errors:
591
+ raise ValueError(f"invalid approval signature: {approval_errors[0]['code']}")
592
+ return approval
593
+
594
+
595
+ def _leaf_payload(entry: Mapping[str, Any]) -> dict[str, Any]:
596
+ return {
597
+ "rule_set_fingerprint": entry["rule_set_fingerprint"],
598
+ "rule_class": entry["rule_class"],
599
+ "visibility": entry["visibility"],
600
+ "approval_fingerprints": entry["approval_fingerprints"],
601
+ }
602
+
603
+
604
+ def rule_anchor_leaf_hash(entry: Mapping[str, Any]) -> str:
605
+ """Hash one rule-batch entry with explicit leaf domain separation."""
606
+
607
+ canonical = canonical_json_dumps(_leaf_payload(entry)).encode("utf-8")
608
+ return sha256_fingerprint_bytes(MERKLE_LEAF_DOMAIN + canonical)
609
+
610
+
611
+ def rule_anchor_parent_hash(left: str, right: str) -> str:
612
+ """Hash two Merkle children with explicit internal-node separation."""
613
+
614
+ return sha256_fingerprint_bytes(
615
+ MERKLE_NODE_DOMAIN + fingerprint_bytes(left) + fingerprint_bytes(right)
616
+ )
617
+
618
+
619
+ def _merkle_root_and_proofs(leaf_hashes: Sequence[str]) -> tuple[str, list[list[dict[str, str]]]]:
620
+ if not leaf_hashes:
621
+ raise ValueError("at least one Merkle leaf is required")
622
+
623
+ proofs: list[list[dict[str, str]]] = [[] for _ in leaf_hashes]
624
+ level: list[tuple[str, list[int]]] = [
625
+ (leaf_hash, [index]) for index, leaf_hash in enumerate(leaf_hashes)
626
+ ]
627
+
628
+ while len(level) > 1:
629
+ next_level: list[tuple[str, list[int]]] = []
630
+ for index in range(0, len(level), 2):
631
+ left_hash, left_indices = level[index]
632
+ if index + 1 < len(level):
633
+ right_hash, right_indices = level[index + 1]
634
+ for leaf_index in left_indices:
635
+ proofs[leaf_index].append({"position": "right", "hash": right_hash})
636
+ for leaf_index in right_indices:
637
+ proofs[leaf_index].append({"position": "left", "hash": left_hash})
638
+ combined_indices = left_indices + right_indices
639
+ else:
640
+ right_hash = left_hash
641
+ for leaf_index in left_indices:
642
+ proofs[leaf_index].append({"position": "right", "hash": right_hash})
643
+ combined_indices = left_indices
644
+ next_level.append(
645
+ (rule_anchor_parent_hash(left_hash, right_hash), combined_indices)
646
+ )
647
+ level = next_level
648
+
649
+ return level[0][0], proofs
650
+
651
+
652
+ def verify_rule_anchor_proof(entry: Mapping[str, Any], merkle_root: str) -> bool:
653
+ """Verify one manifest entry against its declared Merkle root."""
654
+
655
+ current = rule_anchor_leaf_hash(entry)
656
+ if current != entry.get("leaf_hash"):
657
+ return False
658
+ proof = entry.get("proof")
659
+ if not isinstance(proof, list):
660
+ return False
661
+ for node in proof:
662
+ if not isinstance(node, dict):
663
+ return False
664
+ sibling = node.get("hash")
665
+ position = node.get("position")
666
+ if not isinstance(sibling, str) or not FINGERPRINT_RE.fullmatch(sibling):
667
+ return False
668
+ if position == "left":
669
+ current = rule_anchor_parent_hash(sibling, current)
670
+ elif position == "right":
671
+ current = rule_anchor_parent_hash(current, sibling)
672
+ else:
673
+ return False
674
+ return current == merkle_root
675
+
676
+
677
+ def build_rule_anchor_batch(
678
+ rule_sets: Sequence[Mapping[str, Any]],
679
+ approvals: Sequence[Mapping[str, Any]],
680
+ ) -> dict[str, Any]:
681
+ """Build a deterministic Merkle batch after validating approvals."""
682
+
683
+ if not rule_sets:
684
+ raise ValueError("at least one frozen rule set is required")
685
+
686
+ indexed_rules: dict[str, Mapping[str, Any]] = {}
687
+ for rule_set in rule_sets:
688
+ validation_errors = validate_frozen_rule_set_payload(dict(rule_set))
689
+ if validation_errors:
690
+ raise ValueError(f"invalid frozen rule set: {validation_errors[0]['code']}")
691
+ fingerprint = str(rule_set["fingerprint"])
692
+ if fingerprint in indexed_rules:
693
+ raise ValueError("duplicate frozen rule-set fingerprint")
694
+ indexed_rules[fingerprint] = rule_set
695
+
696
+ indexed_approvals: dict[str, list[Mapping[str, Any]]] = {}
697
+ seen_approval_fingerprints: set[str] = set()
698
+ for approval in approvals:
699
+ validation_errors = validate_rule_approval_payload(dict(approval))
700
+ if validation_errors:
701
+ raise ValueError(f"invalid rule approval: {validation_errors[0]['code']}")
702
+ approval_fingerprint = str(approval["fingerprint"])
703
+ if approval_fingerprint in seen_approval_fingerprints:
704
+ raise ValueError("duplicate rule-approval fingerprint")
705
+ seen_approval_fingerprints.add(approval_fingerprint)
706
+ rule_fingerprint = str(approval["rule_set_fingerprint"])
707
+ if rule_fingerprint not in indexed_rules:
708
+ raise ValueError("approval references a rule set outside this batch")
709
+ indexed_approvals.setdefault(rule_fingerprint, []).append(approval)
710
+
711
+ entries: list[dict[str, Any]] = []
712
+ chain_contract_pairs: set[tuple[int, str]] = set()
713
+ for rule_fingerprint in sorted(indexed_rules):
714
+ rule_set = indexed_rules[rule_fingerprint]
715
+ governance = rule_set["governance"]
716
+ authorized = {
717
+ str(address).lower() for address in governance["authorized_signers"]
718
+ }
719
+ rule_approvals = indexed_approvals.get(rule_fingerprint, [])
720
+ unique_approved_signers: set[str] = set()
721
+ for approval in rule_approvals:
722
+ signer = str(approval["signer"]).lower()
723
+ if signer not in authorized:
724
+ raise ValueError("approval signer is not authorized by the frozen rule set")
725
+ if signer in unique_approved_signers:
726
+ raise ValueError("multiple approvals from the same signer are not allowed")
727
+ unique_approved_signers.add(signer)
728
+ chain_contract_pairs.add(
729
+ (int(approval["chain_id"]), str(approval["verifying_contract"]).lower())
730
+ )
731
+ if len(unique_approved_signers) < int(governance["approval_threshold"]):
732
+ raise ValueError("frozen rule set does not meet its approval threshold")
733
+
734
+ entry = {
735
+ "rule_set_fingerprint": rule_fingerprint,
736
+ "rule_class": rule_set["rule_class"],
737
+ "visibility": rule_set["visibility"],
738
+ "approval_fingerprints": sorted(
739
+ str(approval["fingerprint"]) for approval in rule_approvals
740
+ ),
741
+ }
742
+ entry["leaf_hash"] = rule_anchor_leaf_hash(entry)
743
+ entries.append(entry)
744
+
745
+ if len(chain_contract_pairs) != 1:
746
+ raise ValueError("all approvals in a batch must bind to one chain and contract")
747
+ chain_id, verifying_contract = next(iter(chain_contract_pairs))
748
+
749
+ merkle_root, proofs = _merkle_root_and_proofs(
750
+ [str(entry["leaf_hash"]) for entry in entries]
751
+ )
752
+ for entry, proof in zip(entries, proofs, strict=True):
753
+ entry["proof"] = proof
754
+
755
+ visibility_mask = 0
756
+ if any(entry["visibility"] == "public" for entry in entries):
757
+ visibility_mask |= 1
758
+ if any(entry["visibility"] == "private_commitment" for entry in entries):
759
+ visibility_mask |= 2
760
+
761
+ batch: dict[str, Any] = {
762
+ "schema_version": ANCHOR_BATCH_SCHEMA,
763
+ "type": "rule_anchor_batch",
764
+ "batch_id": f"rule-anchor-batch:{merkle_root[7:31]}",
765
+ "hash_algorithm": "sha256",
766
+ "merkle_scheme": "core.sha256_merkle.v1",
767
+ "chain_id": chain_id,
768
+ "verifying_contract": verifying_contract,
769
+ "rule_set_count": len(entries),
770
+ "visibility_mask": visibility_mask,
771
+ "entries": entries,
772
+ "merkle_root": merkle_root,
773
+ }
774
+ batch["manifest_fingerprint"] = artifact_fingerprint(batch, "manifest_fingerprint")
775
+ return batch
776
+
777
+
778
+ def validate_rule_anchor_batch_payload(payload: Any) -> list[dict[str, Any]]:
779
+ """Validate a batch manifest, every leaf, and every Merkle proof."""
780
+
781
+ errors = _schema_errors(payload, "rule_anchor_batch.v1.json")
782
+ if not isinstance(payload, dict):
783
+ return errors
784
+
785
+ entries = payload.get("entries")
786
+ root = payload.get("merkle_root")
787
+ if isinstance(entries, list):
788
+ fingerprints = [
789
+ entry.get("rule_set_fingerprint")
790
+ for entry in entries
791
+ if isinstance(entry, dict)
792
+ ]
793
+ if len(fingerprints) != len(set(fingerprints)):
794
+ errors.append(
795
+ error(
796
+ "duplicate_rule_set_fingerprint",
797
+ "Batch entries must reference unique rule sets.",
798
+ "entries",
799
+ )
800
+ )
801
+ if fingerprints != sorted(fingerprints):
802
+ errors.append(
803
+ error(
804
+ "noncanonical_entry_order",
805
+ "Batch entries must be sorted by rule_set_fingerprint.",
806
+ "entries",
807
+ )
808
+ )
809
+ if payload.get("rule_set_count") != len(entries):
810
+ errors.append(
811
+ error(
812
+ "rule_set_count_mismatch",
813
+ "rule_set_count must equal the number of entries.",
814
+ "rule_set_count",
815
+ )
816
+ )
817
+
818
+ expected_mask = 0
819
+ for index, entry in enumerate(entries):
820
+ if not isinstance(entry, dict):
821
+ continue
822
+ approval_fingerprints = entry.get("approval_fingerprints")
823
+ if isinstance(approval_fingerprints, list):
824
+ if approval_fingerprints != sorted(approval_fingerprints):
825
+ errors.append(
826
+ error(
827
+ "noncanonical_approval_order",
828
+ "approval_fingerprints must be sorted.",
829
+ f"entries.{index}.approval_fingerprints",
830
+ )
831
+ )
832
+ if len(approval_fingerprints) != len(set(approval_fingerprints)):
833
+ errors.append(
834
+ error(
835
+ "duplicate_approval_fingerprint",
836
+ "approval_fingerprints must be unique per entry.",
837
+ f"entries.{index}.approval_fingerprints",
838
+ )
839
+ )
840
+ if entry.get("visibility") == "public":
841
+ expected_mask |= 1
842
+ if entry.get("visibility") == "private_commitment":
843
+ expected_mask |= 2
844
+ if isinstance(root, str) and FINGERPRINT_RE.fullmatch(root):
845
+ try:
846
+ proof_valid = verify_rule_anchor_proof(entry, root)
847
+ except (KeyError, TypeError, ValueError):
848
+ proof_valid = False
849
+ if not proof_valid:
850
+ errors.append(
851
+ error(
852
+ "invalid_merkle_proof",
853
+ "Entry leaf or proof does not resolve to merkle_root.",
854
+ f"entries.{index}.proof",
855
+ )
856
+ )
857
+ if payload.get("visibility_mask") != expected_mask:
858
+ errors.append(
859
+ error(
860
+ "visibility_mask_mismatch",
861
+ "visibility_mask does not match batch entry visibility.",
862
+ "visibility_mask",
863
+ )
864
+ )
865
+
866
+ if isinstance(root, str) and FINGERPRINT_RE.fullmatch(root):
867
+ expected_batch_id = f"rule-anchor-batch:{root[7:31]}"
868
+ if payload.get("batch_id") != expected_batch_id:
869
+ errors.append(
870
+ error(
871
+ "batch_id_mismatch",
872
+ "batch_id must be derived from the Merkle root.",
873
+ "batch_id",
874
+ )
875
+ )
876
+
877
+ declared = payload.get("manifest_fingerprint")
878
+ if isinstance(declared, str) and FINGERPRINT_RE.fullmatch(declared):
879
+ expected = artifact_fingerprint(payload, "manifest_fingerprint")
880
+ if declared != expected:
881
+ errors.append(
882
+ error(
883
+ "manifest_fingerprint_mismatch",
884
+ "Batch manifest fingerprint does not match canonical content.",
885
+ "manifest_fingerprint",
886
+ declared=declared,
887
+ computed=expected,
888
+ )
889
+ )
890
+ return errors
891
+
892
+
893
+ def encode_anchor_rule_batch_calldata(batch: Mapping[str, Any]) -> str:
894
+ """ABI-encode ``anchorRuleBatch`` without requiring a wallet library."""
895
+
896
+ errors = validate_rule_anchor_batch_payload(dict(batch))
897
+ if errors:
898
+ raise ValueError(f"invalid rule anchor batch: {errors[0]['code']}")
899
+
900
+ count = int(batch["rule_set_count"])
901
+ mask = int(batch["visibility_mask"])
902
+ if count > (2**32 - 1):
903
+ raise ValueError("rule_set_count exceeds uint32")
904
+ if mask > 255:
905
+ raise ValueError("visibility_mask exceeds uint8")
906
+ encoded = "".join(
907
+ (
908
+ ANCHOR_RULE_BATCH_SELECTOR,
909
+ fingerprint_bytes(str(batch["merkle_root"])).hex(),
910
+ fingerprint_bytes(str(batch["manifest_fingerprint"])).hex(),
911
+ count.to_bytes(32, "big").hex(),
912
+ mask.to_bytes(32, "big").hex(),
913
+ )
914
+ )
915
+ return "0x" + encoded
916
+
917
+
918
+ def build_unsigned_rule_anchor_request(
919
+ batch: Mapping[str, Any],
920
+ submitter: str,
921
+ *,
922
+ nonce: int | None = None,
923
+ gas_limit: int | None = None,
924
+ max_fee_per_gas_wei: int | None = None,
925
+ max_priority_fee_per_gas_wei: int | None = None,
926
+ gas_price_wei: int | None = None,
927
+ reserve_batches: int = 4,
928
+ safety_multiplier_bps: int = 12_500,
929
+ observed_balance_wei: int | None = None,
930
+ contract_code_verified: bool | None = None,
931
+ ) -> dict[str, Any]:
932
+ """Build an unsigned transaction and native-gas reserve calculation."""
933
+
934
+ if not ADDRESS_RE.fullmatch(submitter):
935
+ raise ValueError("submitter must be an EVM address")
936
+ if reserve_batches < 1:
937
+ raise ValueError("reserve_batches must be at least 1")
938
+ if safety_multiplier_bps < 10_000:
939
+ raise ValueError("safety_multiplier_bps cannot be below 10000")
940
+ if gas_price_wei is not None and max_fee_per_gas_wei is not None:
941
+ raise ValueError("legacy gas_price_wei and EIP-1559 max fee are mutually exclusive")
942
+
943
+ errors = validate_rule_anchor_batch_payload(dict(batch))
944
+ if errors:
945
+ raise ValueError(f"invalid rule anchor batch: {errors[0]['code']}")
946
+
947
+ fee_per_gas = max_fee_per_gas_wei if max_fee_per_gas_wei is not None else gas_price_wei
948
+ per_batch_max_cost: int | None = None
949
+ required_balance: int | None = None
950
+ shortfall: int | None = None
951
+ sufficient: bool | None = None
952
+ max_cost_per_rule: int | None = None
953
+ if gas_limit is not None and fee_per_gas is not None:
954
+ raw_batch_cost = gas_limit * fee_per_gas
955
+ per_batch_max_cost = (raw_batch_cost * safety_multiplier_bps + 9_999) // 10_000
956
+ required_balance = per_batch_max_cost * reserve_batches
957
+ count = int(batch["rule_set_count"])
958
+ max_cost_per_rule = (per_batch_max_cost + count - 1) // count
959
+ if observed_balance_wei is not None:
960
+ sufficient = observed_balance_wei >= required_balance
961
+ shortfall = max(0, required_balance - observed_balance_wei)
962
+
963
+ if sufficient is False:
964
+ readiness = "insufficient_balance"
965
+ elif contract_code_verified is not True:
966
+ readiness = "contract_unverified"
967
+ elif sufficient is True:
968
+ readiness = "ready"
969
+ elif gas_limit is None or fee_per_gas is None:
970
+ readiness = "offline_unpriced"
971
+ else:
972
+ readiness = "balance_unobserved"
973
+
974
+ request: dict[str, Any] = {
975
+ "schema_version": UNSIGNED_TRANSACTION_SCHEMA,
976
+ "type": "unsigned_rule_anchor_transaction",
977
+ "signing_mode": "external_wallet_only",
978
+ "broadcast": False,
979
+ "batch_manifest_fingerprint": batch["manifest_fingerprint"],
980
+ "rule_set_count": batch["rule_set_count"],
981
+ "transaction": {
982
+ "from": submitter.lower(),
983
+ "to": str(batch["verifying_contract"]).lower(),
984
+ "chain_id": batch["chain_id"],
985
+ "value_wei": 0,
986
+ "data": encode_anchor_rule_batch_calldata(batch),
987
+ "nonce": nonce,
988
+ "gas_limit": gas_limit,
989
+ "max_fee_per_gas_wei": max_fee_per_gas_wei,
990
+ "max_priority_fee_per_gas_wei": max_priority_fee_per_gas_wei,
991
+ "gas_price_wei": gas_price_wei,
992
+ },
993
+ "gas_reserve": {
994
+ "unit": "native_wei",
995
+ "reserve_batches": reserve_batches,
996
+ "safety_multiplier_bps": safety_multiplier_bps,
997
+ "per_batch_max_cost_wei": per_batch_max_cost,
998
+ "max_cost_per_rule_wei": max_cost_per_rule,
999
+ "required_balance_wei": required_balance,
1000
+ "observed_balance_wei": observed_balance_wei,
1001
+ "shortfall_wei": shortfall,
1002
+ "sufficient": sufficient,
1003
+ },
1004
+ "contract_code_verified": contract_code_verified,
1005
+ "readiness": readiness,
1006
+ "warnings": [
1007
+ "This artifact is unsigned and must be reviewed and signed by an external wallet.",
1008
+ "CORE never requests, receives, stores, or transmits wallet secrets.",
1009
+ "Gas reserve is advisory and denominated only in the network native asset.",
1010
+ ],
1011
+ }
1012
+ request["fingerprint"] = artifact_fingerprint(request)
1013
+ schema_errors = _schema_errors(request, "unsigned_rule_anchor_transaction.v1.json")
1014
+ if schema_errors:
1015
+ raise ValueError(f"unsigned transaction schema error: {schema_errors[0]['message']}")
1016
+ return request
1017
+
1018
+
1019
+ def validate_unsigned_rule_anchor_request_payload(payload: Any) -> list[dict[str, Any]]:
1020
+ """Validate an unsigned transaction and recompute every derived value."""
1021
+
1022
+ errors = _schema_errors(payload, "unsigned_rule_anchor_transaction.v1.json")
1023
+ if not isinstance(payload, dict):
1024
+ return errors
1025
+
1026
+ transaction = payload.get("transaction")
1027
+ reserve = payload.get("gas_reserve")
1028
+ if not isinstance(transaction, dict) or not isinstance(reserve, dict):
1029
+ return errors
1030
+
1031
+ errors.extend(_wallet_secret_errors(payload, "Unsigned transaction"))
1032
+
1033
+ max_fee = transaction.get("max_fee_per_gas_wei")
1034
+ priority_fee = transaction.get("max_priority_fee_per_gas_wei")
1035
+ gas_price = transaction.get("gas_price_wei")
1036
+ if max_fee is not None and gas_price is not None:
1037
+ errors.append(
1038
+ error(
1039
+ "mutually_exclusive_fee_modes",
1040
+ "EIP-1559 max fee and legacy gas price cannot both be set.",
1041
+ "transaction",
1042
+ )
1043
+ )
1044
+ if (
1045
+ isinstance(max_fee, int)
1046
+ and not isinstance(max_fee, bool)
1047
+ and isinstance(priority_fee, int)
1048
+ and not isinstance(priority_fee, bool)
1049
+ and priority_fee > max_fee
1050
+ ):
1051
+ errors.append(
1052
+ error(
1053
+ "priority_fee_exceeds_max_fee",
1054
+ "max_priority_fee_per_gas_wei cannot exceed max_fee_per_gas_wei.",
1055
+ "transaction.max_priority_fee_per_gas_wei",
1056
+ )
1057
+ )
1058
+
1059
+ data = transaction.get("data")
1060
+ manifest = payload.get("batch_manifest_fingerprint")
1061
+ rule_count = payload.get("rule_set_count")
1062
+ if isinstance(data, str) and re.fullmatch(r"0x[a-f0-9]{264}", data):
1063
+ selector = data[2:10]
1064
+ encoded_manifest = data[74:138]
1065
+ encoded_count = int(data[138:202], 16)
1066
+ if selector != ANCHOR_RULE_BATCH_SELECTOR:
1067
+ errors.append(
1068
+ error(
1069
+ "calldata_selector_mismatch",
1070
+ "Transaction data does not call anchorRuleBatch.",
1071
+ "transaction.data",
1072
+ )
1073
+ )
1074
+ if isinstance(manifest, str) and encoded_manifest != manifest.removeprefix("sha256:"):
1075
+ errors.append(
1076
+ error(
1077
+ "calldata_manifest_mismatch",
1078
+ "Calldata manifest hash does not match batch_manifest_fingerprint.",
1079
+ "transaction.data",
1080
+ )
1081
+ )
1082
+ if isinstance(rule_count, int) and not isinstance(rule_count, bool) and encoded_count != rule_count:
1083
+ errors.append(
1084
+ error(
1085
+ "calldata_rule_count_mismatch",
1086
+ "Calldata rule count does not match rule_set_count.",
1087
+ "transaction.data",
1088
+ )
1089
+ )
1090
+
1091
+ gas_limit = transaction.get("gas_limit")
1092
+ fee_per_gas = max_fee if max_fee is not None else gas_price
1093
+ reserve_batches = reserve.get("reserve_batches")
1094
+ multiplier = reserve.get("safety_multiplier_bps")
1095
+ observed_balance = reserve.get("observed_balance_wei")
1096
+ expected_per_batch: int | None = None
1097
+ expected_per_rule: int | None = None
1098
+ expected_required: int | None = None
1099
+ expected_shortfall: int | None = None
1100
+ expected_sufficient: bool | None = None
1101
+ if all(
1102
+ isinstance(value, int) and not isinstance(value, bool)
1103
+ for value in (gas_limit, fee_per_gas, reserve_batches, multiplier, rule_count)
1104
+ ):
1105
+ raw_batch_cost = gas_limit * fee_per_gas
1106
+ expected_per_batch = (raw_batch_cost * multiplier + 9_999) // 10_000
1107
+ expected_per_rule = (expected_per_batch + rule_count - 1) // rule_count
1108
+ expected_required = expected_per_batch * reserve_batches
1109
+ if isinstance(observed_balance, int) and not isinstance(observed_balance, bool):
1110
+ expected_sufficient = observed_balance >= expected_required
1111
+ expected_shortfall = max(0, expected_required - observed_balance)
1112
+
1113
+ derived = {
1114
+ "per_batch_max_cost_wei": expected_per_batch,
1115
+ "max_cost_per_rule_wei": expected_per_rule,
1116
+ "required_balance_wei": expected_required,
1117
+ "shortfall_wei": expected_shortfall,
1118
+ "sufficient": expected_sufficient,
1119
+ }
1120
+ for key, expected in derived.items():
1121
+ if reserve.get(key) != expected:
1122
+ errors.append(
1123
+ error(
1124
+ "gas_reserve_mismatch",
1125
+ f"{key} does not match deterministic gas-reserve calculation.",
1126
+ f"gas_reserve.{key}",
1127
+ declared=reserve.get(key),
1128
+ computed=expected,
1129
+ )
1130
+ )
1131
+
1132
+ if expected_sufficient is False:
1133
+ expected_readiness = "insufficient_balance"
1134
+ elif payload.get("contract_code_verified") is not True:
1135
+ expected_readiness = "contract_unverified"
1136
+ elif expected_sufficient is True:
1137
+ expected_readiness = "ready"
1138
+ elif gas_limit is None or fee_per_gas is None:
1139
+ expected_readiness = "offline_unpriced"
1140
+ else:
1141
+ expected_readiness = "balance_unobserved"
1142
+ if payload.get("readiness") != expected_readiness:
1143
+ errors.append(
1144
+ error(
1145
+ "readiness_mismatch",
1146
+ "readiness does not match verified contract code, price, and reserve evidence.",
1147
+ "readiness",
1148
+ declared=payload.get("readiness"),
1149
+ computed=expected_readiness,
1150
+ )
1151
+ )
1152
+
1153
+ declared_fingerprint = payload.get("fingerprint")
1154
+ if isinstance(declared_fingerprint, str) and FINGERPRINT_RE.fullmatch(declared_fingerprint):
1155
+ computed_fingerprint = artifact_fingerprint(payload)
1156
+ if declared_fingerprint != computed_fingerprint:
1157
+ errors.append(
1158
+ error(
1159
+ "fingerprint_mismatch",
1160
+ "Unsigned transaction fingerprint does not match canonical content.",
1161
+ "fingerprint",
1162
+ declared=declared_fingerprint,
1163
+ computed=computed_fingerprint,
1164
+ )
1165
+ )
1166
+ return errors
1167
+
1168
+
1169
+ def validate_unsigned_rule_anchor_deployment_payload(payload: Any) -> list[dict[str, Any]]:
1170
+ """Validate an unsigned deployment and recompute all locally provable fields."""
1171
+
1172
+ errors = _schema_errors(payload, "unsigned_rule_anchor_deployment.v1.json")
1173
+ if not isinstance(payload, dict):
1174
+ return errors
1175
+
1176
+ errors.extend(_wallet_secret_errors(payload, "Unsigned deployment"))
1177
+ transaction = payload.get("transaction")
1178
+ reserve = payload.get("gas_reserve")
1179
+ if not isinstance(transaction, dict) or not isinstance(reserve, dict):
1180
+ return errors
1181
+
1182
+ deployer = transaction.get("from")
1183
+ if isinstance(deployer, str) and ADDRESS_RE.fullmatch(deployer) and deployer != deployer.lower():
1184
+ errors.append(
1185
+ error(
1186
+ "noncanonical_deployer_address",
1187
+ "transaction.from must use canonical lowercase hexadecimal.",
1188
+ "transaction.from",
1189
+ )
1190
+ )
1191
+
1192
+ try:
1193
+ build, creation_hex = load_verified_rule_anchor_build()
1194
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
1195
+ errors.append(
1196
+ error(
1197
+ "frozen_contract_build_unverified",
1198
+ "Frozen CoreRuleAnchor build artifacts could not be verified.",
1199
+ "contract_build_fingerprint",
1200
+ reason=exc.__class__.__name__,
1201
+ )
1202
+ )
1203
+ else:
1204
+ expected_build_fingerprint = canonical_fingerprint(build)
1205
+ if payload.get("contract_build_fingerprint") != expected_build_fingerprint:
1206
+ errors.append(
1207
+ error(
1208
+ "contract_build_fingerprint_mismatch",
1209
+ "contract_build_fingerprint does not identify the verified frozen build.",
1210
+ "contract_build_fingerprint",
1211
+ declared=payload.get("contract_build_fingerprint"),
1212
+ computed=expected_build_fingerprint,
1213
+ )
1214
+ )
1215
+ expected_runtime = build.get("runtime_bytecode_sha256")
1216
+ if payload.get("expected_runtime_bytecode_sha256") != expected_runtime:
1217
+ errors.append(
1218
+ error(
1219
+ "runtime_bytecode_fingerprint_mismatch",
1220
+ "Expected runtime bytecode does not match the verified frozen build.",
1221
+ "expected_runtime_bytecode_sha256",
1222
+ declared=payload.get("expected_runtime_bytecode_sha256"),
1223
+ computed=expected_runtime,
1224
+ )
1225
+ )
1226
+ if transaction.get("data") != "0x" + creation_hex:
1227
+ errors.append(
1228
+ error(
1229
+ "deployment_bytecode_mismatch",
1230
+ "transaction.data does not contain the verified creation bytecode.",
1231
+ "transaction.data",
1232
+ )
1233
+ )
1234
+
1235
+ max_fee = transaction.get("max_fee_per_gas_wei")
1236
+ priority_fee = transaction.get("max_priority_fee_per_gas_wei")
1237
+ gas_price = transaction.get("gas_price_wei")
1238
+ if max_fee is not None and gas_price is not None:
1239
+ errors.append(
1240
+ error(
1241
+ "mutually_exclusive_fee_modes",
1242
+ "EIP-1559 max fee and legacy gas price cannot both be set.",
1243
+ "transaction",
1244
+ )
1245
+ )
1246
+ if priority_fee is not None and max_fee is None:
1247
+ errors.append(
1248
+ error(
1249
+ "priority_fee_without_max_fee",
1250
+ "max_priority_fee_per_gas_wei requires max_fee_per_gas_wei.",
1251
+ "transaction.max_priority_fee_per_gas_wei",
1252
+ )
1253
+ )
1254
+ if (
1255
+ isinstance(max_fee, int)
1256
+ and not isinstance(max_fee, bool)
1257
+ and isinstance(priority_fee, int)
1258
+ and not isinstance(priority_fee, bool)
1259
+ and priority_fee > max_fee
1260
+ ):
1261
+ errors.append(
1262
+ error(
1263
+ "priority_fee_exceeds_max_fee",
1264
+ "max_priority_fee_per_gas_wei cannot exceed max_fee_per_gas_wei.",
1265
+ "transaction.max_priority_fee_per_gas_wei",
1266
+ )
1267
+ )
1268
+
1269
+ gas_limit = transaction.get("gas_limit")
1270
+ fee_per_gas = max_fee if max_fee is not None else gas_price
1271
+ post_reserve = reserve.get("post_deployment_reserve_wei")
1272
+ observed_balance = reserve.get("observed_balance_wei")
1273
+ expected_cost: int | None = None
1274
+ expected_required: int | None = None
1275
+ expected_shortfall: int | None = None
1276
+ expected_sufficient: bool | None = None
1277
+ if all(
1278
+ isinstance(value, int) and not isinstance(value, bool)
1279
+ for value in (gas_limit, fee_per_gas, post_reserve)
1280
+ ):
1281
+ expected_cost = gas_limit * fee_per_gas
1282
+ expected_required = expected_cost + post_reserve
1283
+ if isinstance(observed_balance, int) and not isinstance(observed_balance, bool):
1284
+ expected_sufficient = observed_balance >= expected_required
1285
+ expected_shortfall = max(0, expected_required - observed_balance)
1286
+
1287
+ derived = {
1288
+ "deployment_max_cost_wei": expected_cost,
1289
+ "required_balance_wei": expected_required,
1290
+ "shortfall_wei": expected_shortfall,
1291
+ "sufficient": expected_sufficient,
1292
+ }
1293
+ for key, expected in derived.items():
1294
+ if reserve.get(key) != expected:
1295
+ errors.append(
1296
+ error(
1297
+ "gas_reserve_mismatch",
1298
+ f"{key} does not match deterministic deployment-reserve calculation.",
1299
+ f"gas_reserve.{key}",
1300
+ declared=reserve.get(key),
1301
+ computed=expected,
1302
+ )
1303
+ )
1304
+
1305
+ if expected_sufficient is True:
1306
+ expected_readiness = "ready"
1307
+ elif expected_sufficient is False:
1308
+ expected_readiness = "insufficient_balance"
1309
+ elif expected_cost is None:
1310
+ expected_readiness = "offline_unpriced"
1311
+ else:
1312
+ expected_readiness = "balance_unobserved"
1313
+ if payload.get("readiness") != expected_readiness:
1314
+ errors.append(
1315
+ error(
1316
+ "readiness_mismatch",
1317
+ "readiness does not match the recomputed price and balance evidence.",
1318
+ "readiness",
1319
+ declared=payload.get("readiness"),
1320
+ computed=expected_readiness,
1321
+ )
1322
+ )
1323
+
1324
+ warnings = payload.get("warnings")
1325
+ if isinstance(warnings, list):
1326
+ missing_warnings = sorted(set(UNSIGNED_DEPLOYMENT_WARNINGS) - set(warnings))
1327
+ if missing_warnings:
1328
+ errors.append(
1329
+ error(
1330
+ "required_safety_warning_missing",
1331
+ "Unsigned deployment omits a required external-wallet safety warning.",
1332
+ "warnings",
1333
+ missing=missing_warnings,
1334
+ )
1335
+ )
1336
+
1337
+ declared_fingerprint = payload.get("fingerprint")
1338
+ if isinstance(declared_fingerprint, str) and FINGERPRINT_RE.fullmatch(declared_fingerprint):
1339
+ computed_fingerprint = artifact_fingerprint(payload)
1340
+ if declared_fingerprint != computed_fingerprint:
1341
+ errors.append(
1342
+ error(
1343
+ "fingerprint_mismatch",
1344
+ "Unsigned deployment fingerprint does not match canonical content.",
1345
+ "fingerprint",
1346
+ declared=declared_fingerprint,
1347
+ computed=computed_fingerprint,
1348
+ )
1349
+ )
1350
+ return errors
1351
+
1352
+
1353
+ def validate_rule_anchor_chain_evidence_payload(payload: Any) -> list[dict[str, Any]]:
1354
+ """Validate persisted evidence from read-only on-chain verification."""
1355
+
1356
+ errors = _schema_errors(payload, "rule_anchor_chain_evidence.v1.json")
1357
+ if not isinstance(payload, dict):
1358
+ return errors
1359
+ confirmations = payload.get("confirmations")
1360
+ required = payload.get("required_confirmations")
1361
+ if (
1362
+ isinstance(confirmations, int)
1363
+ and not isinstance(confirmations, bool)
1364
+ and isinstance(required, int)
1365
+ and not isinstance(required, bool)
1366
+ and confirmations < required
1367
+ ):
1368
+ errors.append(
1369
+ error(
1370
+ "insufficient_confirmations",
1371
+ "Persisted chain evidence must meet required_confirmations.",
1372
+ "confirmations",
1373
+ )
1374
+ )
1375
+ declared = payload.get("fingerprint")
1376
+ if isinstance(declared, str) and FINGERPRINT_RE.fullmatch(declared):
1377
+ expected = artifact_fingerprint(payload)
1378
+ if declared != expected:
1379
+ errors.append(
1380
+ error(
1381
+ "fingerprint_mismatch",
1382
+ "Chain-evidence fingerprint does not match canonical content.",
1383
+ "fingerprint",
1384
+ declared=declared,
1385
+ computed=expected,
1386
+ )
1387
+ )
1388
+ return errors