Skip to content

fix(trie): proof generation shouldnt encode private bit#303

Open
samlaf wants to merge 1 commit intoseismicfrom
fix--trie-should-not-encode-flagged-storage-private-bool
Open

fix(trie): proof generation shouldnt encode private bit#303
samlaf wants to merge 1 commit intoseismicfrom
fix--trie-should-not-encode-flagged-storage-private-bool

Conversation

@samlaf
Copy link
Contributor

@samlaf samlaf commented Feb 6, 2026

Not 100% sure about this one, but it seems like a bug, so opening and keeping as a DRAFT for now.

StorageProof's verify function encodes using the U256 value without the is_private bit.

There trying to verify proofs that get returned from eth_getProof endpoint currently fails for private slots. We want to completely disallow generating proofs for private slots anyways, but this the code semantics should be clearer/cleaner.

Implementation Note

In the seismic-trie the is-private bool is actually encoded as part of the key! Which is why it doesn't need to be concatenated to the value. Either would work, but I personally would err on the side of putting it in the value, even at the cost of wasting 1 extra byte, since we probably could then use the stock mpt trie library without needing to fork it I think...

Reproducible test

Here's the test I was playing with to figure this out. That being said it's coming from an eth_getProof call that was returning a proof for a private slot, which we want to disallow... but still leaving here for now.

    #[test]
    #[cfg(feature = "eip1186")]
    fn test_verify_eth_get_proof_response() {
        use alloy_rpc_types_eth::EIP1186StorageProof;

        let json = r#"{
            "address": "0x14ab2445e5d3349cb9eeb5f49073c892b7c697db",
            "balance": "0x0",
            "codeHash": "0x77f0291bf2426f5ec9952a825259d54db3ad3afef0e8e7766b6c57907233d98a",
            "nonce": "0x1",
            "storageHash": "0xfdeba2ede17b180a781e6184187ea5b5ed2b3398276fd3f7b2ae3a4da868a3f9",
            "accountProof": [
                "0xf90211a082dedd23ec958ba650773353dc3286f640a78ebc03eea94433c960d3aa397810a0747748462dee7da3577e3fcf7a34681e8de1d16826e4f4668b10afea767ddf67a081179369c368234d720773ce2f004a5364aaa1c4d1d630d266c73409eea9f8d3a04a80d5721d84785943fbd61a200251b6486d730d324ef25372d83a678905e723a060034d19f272d974be0950f04a9148288829431df73f32917eb033659aeee36ea06e04304a879c3c786d23ccfa974f230c3855f57226d61b45c3692a568271a636a0ce93ec2fe6082cbac4d790dbce6dfef09e87faeace358d27692c7bdbe06592e1a021d516f8633226d3af591d8844f8f16b0f928916a76b41a62b1f838a37866f5fa08d3da64b3e1453361f4b1cbb6c48ba42e5449dfc949f8a7cd8faf6ae8f041978a0a3f1b564e67eb6ceac872123848a8b042e341ca5726980517fb041f5b5b60b22a068e313c22bc298e83baa8234c35a95f08e1c78a8ff7f503a4cd901b49191740aa0e9f103ef104f64bf47a372b3953aaa303c5ae7d1154c66ccf4991f6aa092de21a09b40c6c81d1a535e94a0febaa4f8ef4d5428e6931a6c51419adcd5980de2d650a0030783eeb6764f29131f9cecbfa1744ac2fa0f622ea627ae9274f4c252402c9fa01d6902b151bf4c59f172fb83f994bc05ab13866c9ad35c28f553dc8482a8e215a08bef9be9dae08af88d53196c5bd20b086e8fde143c0644fe7fd8f716d5c6da3c80",
                "0xf901f180a0c58174613633afef4a38b95c736f0c62b7ce84877a7bceef40a3624a732f111aa062a91fa703e19bce037b3d8b0edd353c986f7929899ebf6cb70415c5c09e549ca05740863a2bf1a546411ac694ff390bff31d1f44b2e401e9cc181143e9dfcd83aa054fe974b786c9b275b6beb837451fb3469c8c8684e9578067a0f024885087449a01a995d70e71474ea5fcb5a3384f4cb3f32bc4cb7247fcc7350638941d9c30d3ca0e032917bbab84f16e4d09bc37a8deab1996bed1f3616fe6b4440942bd46ad835a0b4d312ee36f0d67959d971e4513334e3ba66dcaa10d501fb6310ae55c73f381da0603afde5c9c7740320dcac0acc79f361c3f377132bf25aba50567303cd38fd14a0ffecbaf754a7d4d900b5a5a0b559e3ad4004a0118e9b2d58ce83dba774b416c9a00bb727ceee78ceba165937f81f3db1bf51834b5bf993f1617ad312d7c0cbe82da01bf0a331e15108faba0dd96ab397c4c47dc82a6ed3a2d2e2c1a24861f07cbd4aa044418bc8236feb651d24b6d60026fd654903f11ce368f7721b0eb16e474fa23fa09e8a955f9bd5526159847b73ba1dcc5dca36d39e87e397f4d92497d66dec1529a01a107e5842883d2e99ffb38de57c58bbe174cbbe021868a0b3501c15017f6081a04eee6a5f29059d8f576f1d35a2184df2b364d1fba2223319fb3fafa934642ed480",
                "0xf87180a0971a25bdf3f21a23823c17471ce3857ab57d245b3321d89ff16da26e9c8ea5f1808080808080a0a4549ff1d46c3dad91d6d8e3a20eb694b87b3dc49ac894accdf509d81aca0ed08080808080a00ab727c5586546d4145ae54000d9c8592584fc6c21fd941cf969cf5bcf5470ab8080",
                "0xf8689f3554cf832e9f448a2f092af4dc5664fcfe5bf4ec775530c72426813960dbcab846f8440180a0fdeba2ede17b180a781e6184187ea5b5ed2b3398276fd3f7b2ae3a4da868a3f9a077f0291bf2426f5ec9952a825259d54db3ad3afef0e8e7766b6c57907233d98a"
            ],
            "storageProof": [
                {
                    "key": "0x0000000000000000000000000000000000000000000000000000000000000002",
                    "value": "0x1363156bbee3016d7035b5f6",
                    "proof": [
                        "0xf8f1a0bd26284a1cd51755aaac0c44306a65703780762ebc18b64f7536c10dda19627b80a0a480ad40c92ea0921a58865e46d4190d4747c4c8ecac1f84439a6eaf804b46ee80a058afaa0d038558b439fe04970162610811a9eb8b9b533a9da7a8022681d1254980808080a0eb9939d567b0f19252061183b3bf0282ce147a0efa57111946f46f976e39539180a041ed3e8912b6a57a673c8d088e99e9e13584733e01437376c535aecdaebfaaaba03376808818fade68d4f8344b527af274579a3d7141ea7cf08efa1c0e6af110cda0e183668126d154ef3c1844d7b42519a0067fa62672a479b1a503bd1ab527a9cf808080",
                        "0xf851a074a7ecf0acd3abf6211ce4eb2df8ebfa3b9cc92743366b067cb6e82fb482053e8080808080808080a048f13d881e8393bc9da6c4c061851de33449d403d0c3aeb3fe24131fc4a705d380808080808080",
                        "0xf0a0605787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace8e8c1363156bbee3016d7035b5f601"
                    ]
                }
            ]
        }"#;

        let response: alloy_rpc_types_eth::EIP1186AccountProofResponse =
            serde_json::from_str(json).expect("failed to deserialize proof response");

        // Derive the state root from the first account proof node (keccak256 of the root trie
        // node).
        let state_root = keccak256(&response.account_proof[0]);

        let mut account_proof = AccountProof::from_eip1186_proof(response);

        // The proof data comes from a seismic node where this storage slot is private
        // (the leaf node has a trailing 0x01 privacy flag). EIP-1186 doesn't have a privacy
        // concept, so we must set it manually after conversion.
        for sp in &mut account_proof.storage_proofs {
            sp.is_private = true;
        }

        assert_eq!(account_proof.verify(state_root), Ok(()));
    }

@samlaf samlaf requested a review from cdrappi as a code owner February 6, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant