diff --git a/citrate_sdk/_chain_guard.py b/citrate_sdk/_chain_guard.py index f14ea3c..334bd96 100644 --- a/citrate_sdk/_chain_guard.py +++ b/citrate_sdk/_chain_guard.py @@ -8,7 +8,7 @@ chain once per manager, then sends with ``chainId`` in the tx so an honest node also rejects a mismatch itself. -r1005 and r1006 share chain id 40204, so the chain id alone cannot tell them +r1005, r1006 and r1007 share chain id 40204, so the chain id alone cannot tell them apart. ``assert_genesis`` reads block 0 (``eth_getBlockByNumber``) and compares its hash with the genesis of the vendored address book; ``pinned_send`` and ``CitrateClient`` run it once before the first send. diff --git a/citrate_sdk/_generated/federation_contract.json b/citrate_sdk/_generated/federation_contract.json index 2f053f2..8bfe2c1 100644 --- a/citrate_sdk/_generated/federation_contract.json +++ b/citrate_sdk/_generated/federation_contract.json @@ -6,7 +6,7 @@ "rpcUrl": "https://rpc.citrate.ai", "explorerUrl": "https://explorer.citrate.ai", "wsUrl": "wss://rpc.citrate.ai/ws", - "genesisHash": "0xa3651a272fe411948cbb4cf7cca4cee44be4248d32ab4376cea7636821c6cdd4" + "genesisHash": "0xcf68634502b4fca1e977ac8dd35aa006bb40c04208d5cd55f1c2b508bba96631" }, "contracts": { "ModelRegistry": "0x4c367f7742059b5bf39f6fbe5837ce6164afd33f", @@ -77,7 +77,35 @@ "NetworkSBT": "0x7e7a5a040efc5d7f1900ecc25e0e7f775b417345", "NetworkSBTImpl": "0x01d34046343a171ec7cd4db8978adbab095ef955", "OrganizationSBT": "0x960bc81a48f29cf1912ac29b2b15203ebd35dda6", - "SkillRegistry": "0xbedad7a474785ecb27fc0aa02ca583bdb3761a49" + "SkillRegistry": "0xbedad7a474785ecb27fc0aa02ca583bdb3761a49", + "AppRegistry": "0xe6fa75fb5aca6b0824edd5cfd3efeaa5d82ea2e4", + "AuditBundleRegistry": "0x784a6fbc97766b000998b00e38ad1110461ca984", + "CapabilityGrant": "0xcc0e43ba3cee0de6a5d1942bce41d1d48584671e", + "ClassificationRegistry": "0xdbf07d10f27011d0a7bbb0e73804393854c4ff11", + "ContradictionLedger": "0xbe6d4e2ccb72181588c25e0689ec5a662f7fea95", + "CrossOrgEnvelope": "0x5685fc31158553d7f93f01c7ee0f5871eaeb2949", + "CrossOrgIndex": "0xb35adeca982129687aa07b03ad446f48bf0db010", + "DefensePrimeComplianceRegistry": "0xabfe0c9d358ce8af37dc4d50aa4eaebdb250c914", + "DefensePrimeFLScopeIndex": "0x9ee2bf0fac5b57ee83431ae8a76a3085d846212b", + "EntityRegistry": "0x7cac6481c4352a00a443da2d07bc558282bd5099", + "GovernanceProtocolFactory": "0xa37244e0097e02328fd5e32d1392e72f49edbda1", + "GovernanceTemplateRegistry": "0xbbc275117f157affdf7a50c067dfa1cf73baa5bf", + "MeetingRegistry": "0x3295ba290f983071fa6924a91ec6417a11fe4b4c", + "MoqRegistry": "0x019b077cb8188150fa1d563e01107d13e827a574", + "MultiSigEnvelope": "0x7e893a47434486f39bfa4cd157e5346315371f48", + "PartProvenanceRegistry": "0x5d8362d8ebdf4a6a20a7164d7865f6ed30d40142", + "PolicyBinding": "0x2e7b88f0c5ced218c9a825ef2510900aaa625923", + "Quorum.AnchorRegistry": "0x902d63e13fadd0669ad8ebfc03fbfe78f8fc4a21", + "ReleaseManifestRegistry": "0xd6a8b3134340ca1e78843db5eb6640b1dbbc8b20", + "RoleEscalation": "0x6c3f7903d12c28b976d8170e3a5639ec420cb65c", + "RoleGrantTenantIndex": "0x1309f2120e69fd9598ccf171b70d645033c358e9", + "Sortition": "0xab30a3db0b5904fefe62bd7ec7292bc32f36628e", + "SponsorEvidenceRegistry": "0x2d04435e49393d7816f4e35e5c8406e76ab99cf1", + "SupplierRegistry": "0x8cff9083202b19a8201d5cdeadbfca41fedc5efc", + "TenantHierarchy": "0x92f2bc7e1b33841de1b138fe28cf202222562cc3", + "TinaWorkpaperRegistry": "0x086f128ed3babdfde8b556bb9458216e6e720b52", + "TripwireRegistry": "0x6da8d9fe0647ef68930d917aebdfdd5f379af6e8", + "VoteAllowance": "0xc98df3ede1d913e5c304be9e1239db90f56343a0" }, "aaStack": { "EntryPoint": "0x97d5391a647429233e202f99231743c53a648f3c", @@ -143,12 +171,12 @@ "keyPrefix": "cgk_" }, "provenance": { - "deployer": "0xe40325ba1f4cf362b203d9fe2b8160312e926c11", - "deployedAt": "2026-01-01T00:00:00Z", - "book": "citrate-chain/contracts/addresses/r1006/40204.json", - "genesisSource": "citrate-chain/core/consensus/src/hardening.rs R1006_GENESIS", + "deployer": "0xE92A75519e9915E57d88E4e207e4EbcB3664cf23", + "deployedAt": "2026-11-12T14:00:00Z", + "book": "citrate-chain/contracts/addresses/40204.json", + "genesisSource": "citrate-chain/core/consensus/src/hardening.rs R1007_GENESIS", "sourceHashes": { - "chainAddresses": "724e3843f677588a6aac680554dd1f2db768d5c3ca2bea18bdc0c173c100b90b", + "chainAddresses": "1b908e56f99ef14e0fe260d27e0ce837200ee4bb0884771b5a007ec66a93ca96", "entitlements": "d7c1167466eafbc3d2f8233c2adc32d310b4c73026b998e46e1b4f0c1fa461a1" } } diff --git a/citrate_sdk/client.py b/citrate_sdk/client.py index a3345b7..cc6196f 100644 --- a/citrate_sdk/client.py +++ b/citrate_sdk/client.py @@ -83,9 +83,9 @@ def __init__(self, rpc_url: str = "http://localhost:8545", private_key: str | No (e.g. Ethereum mainnet). Pass an explicit value only when genuinely targeting a different Citrate network. expected_genesis_hash: the block-0 hash the RPC must report before the - client signs a transaction. r1005 and r1006 share chain id 40204, + client signs a transaction. r1005, r1006 and r1007 share chain id 40204, so the chain id alone cannot tell them apart. Defaults to the - vendored address book's genesis (r1006) when ``chain_id`` is the + vendored address book's genesis (r1007 today) when ``chain_id`` is the vendored chain, and to no check for another ``chain_id``. Pass a hash for another network, or ``None`` to skip (a local dev chain). """ diff --git a/citrate_sdk/identity/wallet.py b/citrate_sdk/identity/wallet.py index c5ce97f..e6ebf22 100644 --- a/citrate_sdk/identity/wallet.py +++ b/citrate_sdk/identity/wallet.py @@ -102,7 +102,7 @@ def verify_wallet_address_on_chain( MITM or a wrong-chain RPC cannot make "verified" pass by echoing the publicly computable prediction. - r1006: the same chain id is not the same network (r1005 and r1006 are both 40204). + r1006: the same chain id is not the same network (r1005, r1006 and r1007 are all 40204). When neither ``factory`` nor ``chain_id`` is overridden (the vendored book is being verified), block 0 must be the book's genesis; ``expected_genesis_hash`` overrides it and ``None`` skips it. Raises ``WrongNetworkError`` otherwise. diff --git a/citrate_sdk/learning.py b/citrate_sdk/learning.py index 19478eb..cbcc985 100644 --- a/citrate_sdk/learning.py +++ b/citrate_sdk/learning.py @@ -554,7 +554,7 @@ def _require_address(self) -> str: if not self._staking_address: raise ConfigurationError( "LiquidStakingPool contract address not configured. LiquidStakingPool is not in the " - "r1006 address book (it is not deployed in r1006 block 0), so there is no default; " + "r1007 address book (it is not deployed on r1007, as on r1006), so there is no default; " "pass staking_address only for a network that has it." ) return self._staking_address diff --git a/tests/test_r1006_network_guard.py b/tests/test_r1006_network_guard.py index 6db0fee..76492f7 100644 --- a/tests/test_r1006_network_guard.py +++ b/tests/test_r1006_network_guard.py @@ -1,10 +1,11 @@ -"""r1006 address book + genesis guard. - -r1005 and r1006 share chain id 40204, so the SPY-B-005 / PBA-L6b-042 chain-id pins cannot -tell them apart. The vendored artifact is the r1006 book (citrate-chain -``contracts/addresses/r1006/40204.json`` via citrate-federation gen-contract) and carries the -r1006 block-0 hash (``R1006_GENESIS`` in citrate-chain core/consensus/src/hardening.rs). Every -write path and the on-chain wallet verification read block 0 first and refuse another network. +"""r1007 address book + genesis guard (file name kept from the r1006 version). + +r1005, r1006 and r1007 share chain id 40204, so the SPY-B-005 / PBA-L6b-042 chain-id pins cannot +tell them apart. The vendored artifact is the r1007 root book (citrate-chain +``contracts/addresses/40204.json`` via citrate-federation gen-contract) and carries the r1007 +block-0 hash (``R1007_GENESIS`` in citrate-chain core/consensus/src/hardening.rs). Every write +path and the on-chain wallet verification read block 0 first and refuse another network, +including r1006, which the book just left. """ from __future__ import annotations @@ -22,6 +23,7 @@ from citrate_sdk.identity import wallet from citrate_sdk.learning import StakingManager +R1007 = "0xcf68634502b4fca1e977ac8dd35aa006bb40c04208d5cd55f1c2b508bba96631" R1006 = "0xa3651a272fe411948cbb4cf7cca4cee44be4248d32ab4376cea7636821c6cdd4" R1005 = "0x1dcfc4909dd9fdcb0f03c823d627e4b2032cc86bef5fc4561030d6c64b9775f1" KEY = "0x" + "11" * 32 @@ -31,26 +33,35 @@ # ── the vendored book ──────────────────────────────────────────────────────── -def test_artifact_carries_the_r1006_genesis() -> None: - assert contract.genesis_hash() == R1006 +def test_artifact_carries_the_r1007_genesis() -> None: + assert contract.genesis_hash() == R1007 -def test_book_points_at_r1006_contracts() -> None: +def test_book_points_at_r1007_contracts() -> None: aa = contract.aa_stack() assert aa["CitrateWalletFactory"].lower() == "0x6ee3b59099c9c812cdf1c9e7e6ff166b2558434c" assert aa["CitratePaymaster"].lower() == "0x5a1e2d73f8154a01a78b56e551c977cb8327ff56" assert contract.contracts()["ComputeMarketplace"].lower() == "0x2fec2ac1dde3a1a749c84d487738904cd2ed50c2" -def test_book_drops_contracts_not_in_r1006_block_0() -> None: +def test_book_drops_retired_contracts_and_carries_the_r1007_post_genesis_ones() -> None: names = contract.contracts() - for gone in ("LiquidStakingPool", "CitAgentTimelock", "DefensePrimeComplianceRegistry", "TenantHierarchy"): + for gone in ("LiquidStakingPool", "CitAgentTimelock"): assert gone not in names + # The root book folds the post-genesis contracts in: r1007 POSTGENESIS_DEPLOYER nonce 0. + assert names["TenantHierarchy"].lower() == "0x92f2bc7e1b33841de1b138fe28cf202222562cc3" + assert "DefensePrimeComplianceRegistry" in names + +def test_no_r1006_post_genesis_address_leaks() -> None: + blob = json.dumps(contract.federation_contract()).lower() + for dead in ("0xe1c16749", "0xb0772491", "0x82224833", "0xf2359912", "0x5924d1d3"): + assert dead not in blob, dead -def test_staking_manager_says_liquid_staking_is_not_in_r1006() -> None: - with pytest.raises(ConfigurationError, match="not in the r1006 address book"): - StakingManager(_manager_rpc(R1006)[0], default_account=ACCT).deposit("1") + +def test_staking_manager_says_liquid_staking_is_not_in_r1007() -> None: + with pytest.raises(ConfigurationError, match="not in the r1007 address book"): + StakingManager(_manager_rpc(R1007)[0], default_account=ACCT).deposit("1") def test_no_r1005_address_leaks() -> None: @@ -81,17 +92,17 @@ def rpc(method: str, params: Any = None) -> Any: return c, methods -def test_client_refuses_an_r1005_node_naming_both_hashes() -> None: - c, _ = _client(R1005) +def test_client_refuses_an_r1006_node_naming_both_hashes() -> None: + c, _ = _client(R1006) with pytest.raises(WrongNetworkError) as e: c.verify_network() - assert R1005 in str(e.value) and R1006 in str(e.value) + assert R1006 in str(e.value) and R1007 in str(e.value) assert e.value.error_code == "WRONG_NETWORK" assert isinstance(e.value, CitrateError) -def test_client_accepts_r1006_in_any_case_and_checks_once() -> None: - c, methods = _client(R1006.upper().replace("0X", "0x")) +def test_client_accepts_r1007_in_any_case_and_checks_once() -> None: + c, methods = _client(R1007.upper().replace("0X", "0x")) c.verify_network() c.verify_network() assert methods.count("eth_getBlockByNumber") == 1 @@ -105,7 +116,7 @@ def test_client_fails_closed_without_block_0(genesis: Any) -> None: def test_client_failure_is_not_cached() -> None: - c, methods = _client(R1005) + c, methods = _client(R1006) for _ in range(2): with pytest.raises(WrongNetworkError): c.verify_network() @@ -113,25 +124,25 @@ def test_client_failure_is_not_cached() -> None: def test_client_send_refuses_before_signing_on_another_network() -> None: - c, methods = _client(R1005) + c, methods = _client(R1006) with pytest.raises(WrongNetworkError): c._send_transaction("0x" + "00" * 19 + "01", {"x": 1}) assert "eth_sendRawTransaction" not in methods -def test_client_send_goes_through_on_r1006() -> None: - c, methods = _client(R1006) +def test_client_send_goes_through_on_r1007() -> None: + c, methods = _client(R1007) c._send_transaction("0x" + "00" * 19 + "01", {"x": 1}) assert "eth_sendRawTransaction" in methods def test_client_override_and_opt_out() -> None: - off, methods = _client(R1005, expected_genesis_hash=None) + off, methods = _client(R1006, expected_genesis_hash=None) off.verify_network() assert "eth_getBlockByNumber" not in methods - pinned, _ = _client(R1005, expected_genesis_hash=R1005) + pinned, _ = _client(R1006, expected_genesis_hash=R1006) pinned.verify_network() - dev, dev_methods = _client(R1005, chain_id=31337) + dev, dev_methods = _client(R1006, chain_id=31337) dev.verify_network() assert "eth_getBlockByNumber" not in dev_methods with pytest.raises(CitrateError, match="expected_genesis_hash"): @@ -154,15 +165,15 @@ def rpc(method: str, params: Any) -> Any: return rpc, methods -def test_manager_write_refuses_an_r1005_node_before_sending() -> None: - rpc, methods = _manager_rpc(R1005) +def test_manager_write_refuses_an_r1006_node_before_sending() -> None: + rpc, methods = _manager_rpc(R1006) with pytest.raises(WrongNetworkError): ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS).heartbeat() assert "eth_sendTransaction" not in methods -def test_manager_write_sends_on_r1006_and_checks_once() -> None: - rpc, methods = _manager_rpc(R1006) +def test_manager_write_sends_on_r1007_and_checks_once() -> None: + rpc, methods = _manager_rpc(R1007) mgr = ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS) mgr.heartbeat() mgr.heartbeat() @@ -189,7 +200,7 @@ def rpc(method: str, params: Any) -> Any: def test_manager_failure_is_not_cached() -> None: """A failed genesis check must not mark the manager verified: a second write refuses too.""" - rpc, methods = _manager_rpc(R1005) + rpc, methods = _manager_rpc(R1006) mgr = ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS) for _ in range(2): with pytest.raises(WrongNetworkError): @@ -200,7 +211,7 @@ def test_manager_failure_is_not_cached() -> None: def test_manager_rechecks_after_a_failure_and_then_caches_success() -> None: - rpc, methods = _switching_manager_rpc([R1005, R1006]) + rpc, methods = _switching_manager_rpc([R1006, R1007]) mgr = ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS) with pytest.raises(WrongNetworkError): mgr.heartbeat() @@ -222,11 +233,11 @@ def test_manager_fails_closed_when_block_0_is_unreadable(answer: Any) -> None: def test_manager_override_and_opt_out() -> None: - rpc, methods = _manager_rpc(R1005) + rpc, methods = _manager_rpc(R1006) ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS, expected_genesis_hash=None).heartbeat() assert "eth_getBlockByNumber" not in methods - rpc2, _ = _manager_rpc(R1005) - ComputeManager(rpc2, default_account=ACCT, contract_addresses=ADDRS, expected_genesis_hash=R1005).heartbeat() + rpc2, _ = _manager_rpc(R1006) + ComputeManager(rpc2, default_account=ACCT, contract_addresses=ADDRS, expected_genesis_hash=R1006).heartbeat() # ── verify_wallet_address_on_chain ─────────────────────────────────────────── @@ -254,13 +265,13 @@ def fake_post(url: str, data: str | None = None, headers: Any = None, timeout: A return fake_post -def test_wallet_verify_refuses_an_r1005_node() -> None: - with mock.patch.object(wallet.requests, "post", _wallet_post(R1005)): +def test_wallet_verify_refuses_an_r1006_node() -> None: + with mock.patch.object(wallet.requests, "post", _wallet_post(R1006)): with pytest.raises(WrongNetworkError): wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") -def test_wallet_verify_passes_on_r1006() -> None: - with mock.patch.object(wallet.requests, "post", _wallet_post(R1006)): +def test_wallet_verify_passes_on_r1007() -> None: + with mock.patch.object(wallet.requests, "post", _wallet_post(R1007)): assert wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") == \ wallet.predict_wallet_address(UID)