import base64
import json
import pytest
import acdp
from test_interop import NodeWorker from test_interop import node
REGISTRY_DID = "did:web:registry.example.com"
RECEIPT_KEY_ID = f"{REGISTRY_DID}#receipt-key-1"
REGISTRY_SEED = bytes([0x11] * 32)
LINEAGE = "lin:sha256:c7fef01c000f8edaa9cb46122ceb5d7bca38328f002fb0f40e362e3b289bbb2a"
CTX = "acdp://registry.example.com/12345678-1234-4321-8123-123456781234"
LOG_ID = "did:web:registry.example.com/log/1"
K1_FP = "sha256:139e3940e64b5491722088d9a0d741628fc826e09475d341a780acde3c4b8070"
K2_FP = "sha256:3097e2dee2cb4a34b53840cdb705aed71067c36f68db0e0f559c3f3fa043315f"
RCPT001 = json.dumps(
{
"registry_did": REGISTRY_DID,
"ctx_id": CTX,
"lineage_id": LINEAGE,
"origin_registry": "registry.example.com",
"created_at": "2026-04-16T10:30:15.123Z",
"content_hash": "sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5",
"key_fingerprint": K1_FP,
"signature": {
"algorithm": "ed25519",
"key_id": RECEIPT_KEY_ID,
"value": "vBgQKmn17pHXXY95C07BBeconmjDIdYIvxN5B+YXrQ7tIzFsDNsh1TglzgxOyPUp8lwTz7zwMNiK+Sn5whveDg==",
},
}
)
LHR001 = json.dumps(
{
"receipt_version": "acdp-lhr/1",
"registry_did": REGISTRY_DID,
"lineage_id": LINEAGE,
"head_ctx_id": CTX,
"head_version": 1,
"head_status": "active",
"as_of": "2026-07-04T09:00:00.000Z",
"signature": {
"algorithm": "ed25519",
"key_id": RECEIPT_KEY_ID,
"value": "h4w9cdnmpNXWBkmQQLgbcQ2p22c1wKZCqnHx1sQXE2GuMRP2nlVt+twGikpFPP6zpRCjqEa3UxIxC8Y9qnl7BA==",
},
}
)
LHR_EXPECTED = json.dumps(
{
"authority": "registry.example.com",
"lineage_id": LINEAGE,
"head_ctx_id": CTX,
"head_version": 1,
"head_status": "active",
}
)
LOG001_LEAF_HASHES = [
"sha256:95d99654d4d3de54a4d7cc04e079de61135023c78bb8192bdb79a09253afb8c1",
"sha256:846b4d6c07ca099eea348c1e219345ddd426c0531cc30d3dd626d0fa34ec7704",
"sha256:db94dd74b5c68f6d362129703ea587c8756d65cad0cc9859829021746a114451",
"sha256:dc309b7856483acb5b2a92323dd9c1571a778bdb7b446587100022b49ee5fb3b",
"sha256:6f673f8532d24869047264d89e2ad65f6ff2fa3c2674bb2fb9fa02855e090b3a",
]
LOG001_CHECKPOINT = json.dumps(
{
"checkpoint_version": "acdp-log/1",
"log_id": LOG_ID,
"tree_size": 5,
"root_hash": "sha256:0b5978172c671ca050b44790a749b18fc29d58a7a17495fbb4e0f86eb885f731",
"timestamp": "2026-07-04T12:00:00.000Z",
"signature": {
"algorithm": "ed25519",
"key_id": RECEIPT_KEY_ID,
"value": "o5rJmVE+1w/f7xAvW2P4vHA9FqWcMpS0crUPkMUZKSrBhrCVt/jyS+PCgnHNsNpmr+N+sR9I9qbqQ/Y0ZfOrDQ==",
},
}
)
LOG001_INCLUSION = json.dumps(
{
"log_id": LOG_ID,
"leaf_index": 0,
"tree_size": 5,
"inclusion_path": [
"sha256:846b4d6c07ca099eea348c1e219345ddd426c0531cc30d3dd626d0fa34ec7704",
"sha256:54d7edc4ba9d151eedd7f4bb872884f0af5ff32b39f98866d67873b00687c605",
"sha256:6f673f8532d24869047264d89e2ad65f6ff2fa3c2674bb2fb9fa02855e090b3a",
],
}
)
LOG003_PROOF = json.dumps(
{
"log_id": LOG_ID,
"first_tree_size": 3,
"second_tree_size": 5,
"consistency_path": [
"sha256:db94dd74b5c68f6d362129703ea587c8756d65cad0cc9859829021746a114451",
"sha256:dc309b7856483acb5b2a92323dd9c1571a778bdb7b446587100022b49ee5fb3b",
"sha256:96659974ae162b1243bdf8b32a8f462cfc00c08a43d77574fad5361042d0a1bc",
"sha256:6f673f8532d24869047264d89e2ad65f6ff2fa3c2674bb2fb9fa02855e090b3a",
],
}
)
LOG003_FIRST_ROOT = "sha256:cf4604eee5578b1ca5b9414d901840b1c0e6e275222d3f613301989d20f58e9d"
REV001_BODY = json.dumps(
{
"version": 1,
"supersedes": None,
"agent_id": "did:web:agents.example.com:test-producer",
"contributors": [],
"title": "Key revocation — key-1 compromised",
"summary": (
"Revocation of the Ed25519 key "
"did:web:agents.example.com:test-producer#key-1, compromised "
"since 2026-05-01T00:00:00.000Z."
),
"type": "key-revocation",
"data_refs": [],
"derived_from": [],
"visibility": "public",
"metadata": {
"revoked_key_fingerprint": K1_FP,
"compromised_since": "2026-05-01T00:00:00.000Z",
"reason": "laptop theft; private key material presumed exfiltrated",
},
"acdp_version": "0.3.0",
"content_hash": "sha256:210bb03ec4bd39de893eb7d39ee992913cda80f767b135a02992a71491bf57ca",
"signature": {
"algorithm": "ed25519",
"key_id": "did:web:agents.example.com:test-producer#key-2",
"value": "Lf7P+ZifUGPXIkR2i9Vy4LByaTb6ktsakKcjm4ZFUlcgTs2r9/3eyjDJDNWfT+qAseNYecvYggTIGnT7EZiPAw==",
},
"ctx_id": "acdp://registry.example.com/9f1e2d3c-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"lineage_id": "lin:sha256:6af6229c1c6a4a119695c77e47f6554941aebce3d25ba8567e2ae6ffbb6059cb",
"origin_registry": "registry.example.com",
"created_at": "2026-05-02T08:00:00.000Z",
}
)
def _registry_doc() -> str:
registry = acdp.AcdpProducer.from_seed(REGISTRY_SEED, REGISTRY_DID, RECEIPT_KEY_ID)
raw = base64.b64decode(registry.public_key_b64)
return json.dumps(
{
"id": REGISTRY_DID,
"verificationMethod": [
{
"id": RECEIPT_KEY_ID,
"type": "Ed25519VerificationKey2020",
"controller": REGISTRY_DID,
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": base64.urlsafe_b64encode(raw).rstrip(b"=").decode(),
},
}
],
"assertionMethod": [RECEIPT_KEY_ID],
}
)
NOW = "2026-07-04T12:00:10.000Z"
def test_build_log_leaf_is_byte_identical(node):
py = acdp.AcdpVerifier.build_log_leaf(RCPT001)
nd = node.call("build_log_leaf", receipt_json=RCPT001)["raw"]
assert py == nd
assert acdp.AcdpMerkle.leaf_hash(py) == LOG001_LEAF_HASHES[0]
assert node.call("merkle_leaf_hash", leaf_json=nd)["result"] == (
LOG001_LEAF_HASHES[0]
)
def test_merkle_arithmetic_matches_across_bindings(node):
hashes_json = json.dumps(LOG001_LEAF_HASHES)
py_root = acdp.AcdpMerkle.root_hash(hashes_json)
nd_root = node.call("merkle_root_hash", leaf_hashes_json=hashes_json)["result"]
assert py_root == nd_root
py_node = acdp.AcdpMerkle.node_hash(LOG001_LEAF_HASHES[0], LOG001_LEAF_HASHES[1])
nd_node = node.call(
"merkle_node_hash",
left_hash=LOG001_LEAF_HASHES[0],
right_hash=LOG001_LEAF_HASHES[1],
)["result"]
assert py_node == nd_node
def test_lineage_head_receipt_verdicts_match(node):
doc = _registry_doc()
now = "2026-07-04T09:00:30.000Z"
py = acdp.AcdpVerifier.verify_lineage_head_receipt(LHR001, LHR_EXPECTED, doc, now)
nd = node.call(
"verify_lineage_head_receipt",
receipt_json=LHR001,
expected_json=LHR_EXPECTED,
registry_did_doc_json=doc,
now=now,
)["raw"]
assert py == nd
assert json.loads(py)["valid"]
hostile = json.dumps(dict(json.loads(LHR_EXPECTED), authority="hostile.example"))
py = acdp.AcdpVerifier.verify_lineage_head_receipt(LHR001, hostile, doc, now)
nd = node.call(
"verify_lineage_head_receipt",
receipt_json=LHR001,
expected_json=hostile,
registry_did_doc_json=doc,
now=now,
)["raw"]
assert py == nd
assert not json.loads(py)["valid"]
def test_log_verdicts_match(node):
doc = _registry_doc()
py = acdp.AcdpVerifier.verify_log_checkpoint(LOG001_CHECKPOINT, doc, LOG_ID, NOW)
nd = node.call(
"verify_log_checkpoint",
checkpoint_json=LOG001_CHECKPOINT,
registry_did_doc_json=doc,
expected_log_id=LOG_ID,
now=NOW,
)["raw"]
assert py == nd and json.loads(py)["valid"]
leaf = acdp.AcdpVerifier.build_log_leaf(RCPT001)
py = acdp.AcdpVerifier.verify_log_inclusion(
LOG001_INCLUSION, LOG001_CHECKPOINT, leaf
)
nd = node.call(
"verify_log_inclusion",
inclusion_json=LOG001_INCLUSION,
checkpoint_json=LOG001_CHECKPOINT,
leaf_json=leaf,
)["raw"]
assert py == nd and json.loads(py)["valid"]
py = acdp.AcdpVerifier.verify_log_consistency(
LOG003_PROOF, LOG001_CHECKPOINT, LOG003_FIRST_ROOT
)
nd = node.call(
"verify_log_consistency",
consistency_json=LOG003_PROOF,
checkpoint_json=LOG001_CHECKPOINT,
first_root_hash=LOG003_FIRST_ROOT,
)["raw"]
assert py == nd and json.loads(py)["valid"]
def test_lifecycle_event_minted_in_python_verifies_in_node(node):
p = acdp.AcdpProducer.generate_did_key()
event = {
"event_id": "018f6d0a-7b2e-4c4d-9e1f-3a5b7c9d1e2f",
"ctx_id": CTX,
"event_type": "retracted",
"occurred_at": "2026-07-04T09:15:42.000Z",
"actor": p.agent_did,
}
preimage_hash = acdp.AcdpCanonicalizer.content_hash(json.dumps(event))
event["signature"] = {
"algorithm": "ed25519",
"key_id": p.key_id,
"value": p.sign_challenge(preimage_hash),
}
event_json = json.dumps(event)
py = acdp.AcdpVerifier.verify_lifecycle_event(event_json, None, CTX)
nd = node.call(
"verify_lifecycle_event", event_json=event_json, expected_ctx_id=CTX
)["raw"]
assert py == nd
assert json.loads(py)["valid"]
def test_parsed_revocation_and_classification_match(node):
py_rev = acdp.AcdpVerifier.parse_key_revocation(REV001_BODY, K2_FP)
nd_rev = node.call(
"parse_key_revocation", body_json=REV001_BODY, signer_fingerprint=K2_FP
)["raw"]
assert py_rev == nd_rev
revs = f"[{py_rev}]"
for created_at in ("2026-04-16T10:30:15.123Z", "2026-05-03T09:00:00.000Z", None):
py = acdp.AcdpVerifier.classify_under_revocation(revs, K1_FP, created_at)
nd = node.call(
"classify_under_revocation",
revocations_json=revs,
signer_fingerprint=K1_FP,
receipt_created_at=created_at,
)["raw"]
assert py == nd, f"classification diverged for created_at={created_at!r}"