storageHash Tutorial
Application design for API call for storageHash
If you want to generate proof of a specific state variable of a contract, you will generally need the storageHash,Balance of asset (excluding native token), mapping of ownership, whitelisted accounts, and any other data defined in the contract are all state variables that can generate proof via storageHash
In this storageHash example, we will generate specific NFT tokenId's proof of ownership (which is specific state variable of a contract). The example NFT contract, which uses
ERC721A
, is currently deployed on Ethereum.To store this data in the storage, the
ERC721A.sol
contract uses two lines of code. The first line is a mapping from token ID to ownership details. These ownership details are stored as a struct type that consists of the address of the owner.// Mapping from token ID to ownership details
// An empty struct value does not necessarily mean the token is unowned. See _ownershipOf implementation for details.
mapping(uint256 => TokenOwnership) internal _ownerships;
// Compiler will pack this into a single 256bit word.
struct TokenOwnership {
// The address of the owner.
address addr;
// Keeps track of the start time of ownership with minimal overhead for tokenomics.
uint64 startTimestamp;
// Whether the token has been burned.
bool burned;
}
To obtain the storage key, you first need to determine the slot index of the
mapping(uint256 => TokenOwnership) internal _ownerships
. You can obtain the storage slot index by understanding the contract's storage layout. Refer to this section for a deeper understanding of storage layout. In this example, we use the following storage layout formula to create proofOfOwnership()
.(1) You can obtain the slot index of a mapping using the
hardhat-storage-layout
tool. After obtaining the slot index of the data, you can compute the storage key. Refer to the storage layout for more information.(2) Alternatively, you can calculate the slot index yourself. Based on your understanding of the general rules of the storage layout, if the contract is simple, you can calculate the slot index and obtain the storage key from that.
- The first item in a storage slot is stored lower-order aligned.
- Value types use only as many bytes as are necessary to store them.
- If a value type does not fit the remaining part of a storage slot, it is stored in the next storage slot.
- Structs and array data always start a new slot, and their items are packed tightly according to these rules.
- Items following struct or array data always start a new storage slot.
(3) You will be able to use our storage compute tool (currently a work in progress). 🙂
To verify if a storage key is correct, you can use the RPC method
eth_getStorageAt
to retrieve the value stored under the key.Here are the example parameters we're working with:
- Address:
0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d
(contract address). - Storage key:
0x1d800c2afaac91ef3f6c7512de5ff01cbc7e4109e4356346fb044756afd1a02a
. - Block number:
latest
.
We retrieved the following value from
eth_getStorageAt
: 0x000000000000000062c56c0d355798c2ba0678bd816a8e116246d81caa027eff
.Now let's break down the mapping. We know that it stores a tuple of an address and a timestamp, both packed into 32 bytes of data, with the timestamp taking 12 bytes and the address taking 20 bytes.
The address we're interested in is
0x355798c2ba0678bD816a8e116246D81Caa027eFf
.Therefore, the storage key is:
0x000000000000000062c56c0d-355798c2ba0678bd816a8e116246d81caa027eff
.The first 12 bytes represent a timestamp:
hexToDec(62c56c0d) → 1657105421
.The remaining 20 bytes represent an address:
355798c2ba0678bd816a8e116246d81caa027eff
.In this example, we need to provide proof of a specific variable based on its storage slot. If you don't require a specific variable in your contract, then you can skip this step.
To calculate the keccak hash of the NFT balance state's storage slot, we can create a
proofOfOwnership()
function. The mapping logic is based on the storage layout mapping section. The calculated slot will be used when calling eth_getProof
.export const calculateStorageSlot = async (address:string, token_id: number, contract_address: string, block_number: number, mapping_storage_slot: number) => {
const balance_slot_keccak = ethers.utils.keccak256(
ethers.utils.concat([
ethers.utils.defaultAbiCoder.encode(
['uint256'],
[token_id]
),
ethers.utils.defaultAbiCoder.encode(
['uint256'],
[mapping_storage_slot]
)
])
)
return { blockNum: block_number, slot: balance_slot_keccak }
}
Based on the storage slot calculation you just did, you can now generate proof for a specific storage slot in a specific block number using the
ethGetProof
method.const ethProof = await ethGetProof(address, [slot], blockNum)
The result of
ethProof
looks like this. What we need is storageProof.proof
{
address: '0xb6920bc97984b454a2a76fe1be5e099f461ed9c8',
accountProof: [
'0xf90211a009beb1cc06b5197367da0a64a6a5aabac9872dfb748b4c571ed575ea1a6b1b4da0ae5968bc1bd5188fb831484453dfe713a950e3ccf5ff15177e82ac2552699e22a0f3dc506d7192011a52d5107a44746479ca17212e593217c2500659e1bceb2241a0b3a3a67b65f2761257b6e58a0333ebf1f6bc45d98d4e919f6c793297e3a61619a0f9f44ad754a5febb8c467bf047bed706a201c86346d8697c89a37a2e72a804cea0a2bac938ddc5799089261dd5d2d02e8827d2cc4fc47083b9314139829c648d45a0233e470d44c0ccf88c0003f13dceffc6ed66c0704ada7726003611af402b9629a09bd276f2127ee9ea58fb5a1b8e080fb43850880d921fb151417901f589b07c61a04abca7794fdc79439ff426d065867697b334c74098ee6a7e039139e959c66629a0a7db54646d715dd7f15a95703ccb017f2ab88a49a6575a004130229df02a5555a0c735e8c7068911ec04053224e00842124dda7ea91d32facaedf644af4c26d752a0f049438dd785d10eeb884464a7fa5da310704543ab4cc2848db5dcac009c168ba0c667f305037caf0d3fbc9b7661bfaf556f205061ab3620180537e5cb63470cb3a082304cfcd1f16591a50cddd4cf598a28eef11ec2acd849655495dcbe2d198286a00c9cdf7b445d3a20358d8c080ef5053056409c7e0eb9a85f602c0d6faa5e0c9fa0f1dca60af4eee87d8a94149a3a846e26885011c25adfc6b041ea1766d477e84e80',
'0xf90211a0e1e7d759ac47a755e414739677c89fd319da1dbfdd7d23cb4e2b7b2eebd41eb4a0f635504c70c93c32a6a52d2cc095778583efe204911827c020bb53077f3b2338a09934ed006c0344af1baa93262efeba5d26a2f625eb078dc07a74eed9c0f82a52a08dcb007a65525cb894be1361924cac8c6307df1f3b6a67a6ff2013f866d507a7a0daf3a81d37214b806a181166ddd99be530c5bacd4938537719296cdc4ff1a3aba0325d118ae84f9426cb709fbbbf27fde9bff10da2bfff8621760845cb48369085a00157501155087beceaccd5c1b03d6193deb73a8fb71d6841888f90b33c5f43b0a09d9ea57911f46ad982595b79ce782120ef8849fe60fffd70ac4e9a66a7ffcd68a049ade375f6447b13f6fefbb996fc482f91f77f5ec97c1cefaf65c412717ff149a0d0566dfe26af2546442ee5d0253921c544da553c0a719d6c52938eff9a280596a0a1c86f09876b93f8b076956b96621b06b2e45070239c3d98cbebfe6b4bde6929a0f0ea5c3206d216fdded2d30d11366e713e7fdfde43f7228d2f645e4786ee6cc1a026e6ed40a3b71d9fafcb7b860108cdd27027fcba83c452fc80e464abbf81c352a003626a7e5f3d52d02df2ee5a5195d6498b3103703d983f6644d242a0c4c26d5ba00ae74a9fe500d6e762971abf0f34184ac7d426fe36d0e93fc0083defbc856bd3a03cf63636f66929e213b0ce909e75490cca0ff6fdf5afe8b46ca051b204a4381980',
'0xf90211a0f1effc83c9f1a09c585e9d7eecf35b146d6b0caf448490e099d1bffa719c3353a0bc4dfb6eb251577f385e0a991eb92b03ceeddf4ef91ea4f7d716776465240948a0d4a2cda11c56b1459ac3e6c72edba193266f85986b25ef7583e954fed3702eeda0c139a648c5ca9ca3217bb1f74286b8b1acc15ae25a0f8781691ae053b3799b1ca06f122ec7257af7a77781c8e48179951adcb379ef2f943eb43b1c1656db8c6e86a0a4e8dcf21572a0dda2d12aed43e96966b5e1f62358589af2cf668a04a5b29c4da0fce0c37425054696ea2e8be9b8101bf4059413f228c6a4467f0e18f1ecf006eea09ee46431a54f466035181c20c9c1c8bb2921b55b7b1d3c5888221e499b79c292a07d01e3260b7abf41e2118e6c691f70039f4aca6cbf47d5df6a7940ea16c719cda0b0240c8cc9cab9ff62ce176acab440b2c16c61896cc9205d3e5198d717a0e6e2a0c2cc0b89953cf0b56f53141ed779c9d37cc6b4e8436a0bc2e0628fadd8783521a008debcf0feb2f9a5b91775de30a366a24fa910d216922a27690eaa47af373ecaa0235a1824e9336c4707abb1b9d91cbd21759e4861f11f7d0bd94a3bd2b84c31eea06141190665a53bf1287e3fb8610dc86e018292e82d69d83c94efb07f39a75f2ea0ac744d28d306b3ae6768203fd9a488a70cbd4a01843577d14b9a3c1f56672d6da0b90d3d35d10e43b081978492f06b0d01c7a06cf171a0f4d041dfddc5ccabd7f980',
'0xf90211a017ea63ad36d2dc105de86b2fdcd0594abc1a4a3f7270fe8a99015ac523612d1fa027ec7aa2e4d00ecfdf783da4e56ccc69a12efd5dfb2fb3d846b51f637eeeaea4a050329453db8253b261666df127ab20cd6d817ee8b8ac68422c4d3a279c1527e6a0dc60a6a2ace9fdec4d4b4208164a7fc4e9a632cadb93f307f49ba28772b3d3eca056601a07a54e083baf89ea6146e4b94a63a257096fe9a2041b0c0c199fa6bc69a056afc8085e3a216f3b30e99c18c2d8e144b018e167eb3b1064aa7656bfc778e5a03b63fe500bf201be24eba55f851425b9443f9a3195ce9652f4d4ac1399deb76fa0551b502b55ac032ab49926d4d7e49d75667faca1facae190f9a320b06746e0dea0dfe2c67afdc64348126129daffdd77781e404c3227455f4a310c7023a8b1c0b7a0961e667cb65a7d7111a2b9ee5c2433cc87ecc072a24fa094a02862dfb0a61479a0c6b6117a0ec5764b69fb5e886d46e5cf5235a28b8364e8f9352f15685b7376f2a025204c8325c2e9480eb578c163b5edcba5de5ad2564e5f91f940d3e2fc8651d2a0674e6d0dd316dc5211acda8c3996a0a3d6f7bb11126698445e2412a6c8d9bbd6a0a93ceeec2e94ee1efa3eefc0b324bead44b86bcf6953ebb885fc4c9c81642b5ca0b3f628d274d27a58413aa547ab980c1af15f33b5042053180a84b06d1eb05d35a0765e7c24c9534a6d378fe5ef7f79e383390b29473337d227252ed8f279759a9c80',
'0xf90211a0c8609a71c782a99824e2733e65d57eb7d4b87c5c6fe00d0021db043499223d59a04060cb0608b62f5411f1981f38dbd2dd1aa9abcd0a50165154fb9685a44ce80fa032ea9a0ca67d0600c2c179c9cfd74b1e2b13603526d8925f951b826fcbb3a678a0dee704f6b4adf8e28679334a940168e65310acabe78cedbd89eb61c9e450231ca0a8bcd9c9b7c6f18ef7e1deb5bdb121312d48bb5599ea2622828fc3e61d7a14faa0b87ab11d9d26635b3983d2ec16e954689f0532d32baca1bfd309b1a99af91992a0cdf7ca2dd2d0d1e162dda40fd4cabef4aca7d8e95c0e05b10d852dd1fe325ad6a05e85ac839769fbf22114b5a197b595ef69a3dd1e591ebe541b46be0a8c53458ca079911369a1cbea76b0d41fca4116d5b1e7bacfd9abdce7f659018ad55ea3a05aa07e9b8d73d013c6e544e5e682aa6e3c0daae7177387f964bf7903783658b1ad70a0a1ce36db0d7b155de5b281f57c4d599bdbc8f9e0d3db1dcee94913d1ff03a084a0965f9b1d5a5a8106f0008bb92875cc182a2c617a45acfdf712a7ea08a36de1c2a06a523833488b468bbf25206f86d3743f234ff19fa3de9ae1ab2ac86f445be18fa0b694f65e5037e24f26b8b0c9425195402962e108b451d5fa6410edaf8f2bcdeca0c58fd147f2d67b25dffe7d038108fe1a02947447f09b3fbdb9fe59b6c675f26da01cf61b1d5a370ce5cf92b98960cce4e90dee965b4c14ee9bd14edfaf54a1d4e680',
'0xf90191a008d0368a2b264bc343446c9d85f77852ea4dc9adb65dff96941e480a592dedb9a0f60b6db71a7b88bdffe1190450c39d3683b1da8e2ade5b1aa2f752bd19dd3fc2a04a7e72d132338da42b9ec7a0830a544986324355b689eb1d40f233a340bdf929a0c0fe5500b2dfe6b4defc243dbc609a155842801a265fed29aa965418e44d9976a091d956cb05a481e6434ab6ae77b402b442d9e27dfb4b4d1a1150803ca1e7deeea0e98c87209b91e3a8292e71f99b24834c5e554091578ef33c196102c8572b657f8080a081251153e60068affaca72030aedaeb9dff4827d618160c680c9ad9ff2550134a04cd8ff6915416260e97c6958afb761a8ee885da78da042ac9807bfa95c02b9a6a0eb82c4091ff382331f0e4b82e9f3b9b568d009fbc52c683023c85e9177b11a3ca0b1a4760997645901df598b6ac355a80db4c4c21b82b682d377a8d1ee72972fc280a05f0eb64cfff7333b69f3cc4707fc42ce70a9eee1b1858e9b4deec573dc239ee180a002456143cb4a814e6cf9c2592f071d3b06417f1f83413995d6056450bdab089a80',
'0xf891808080a0094aa4e4c1ee4097f181d17042e42efe9a3eb5610bdd34d8edbb57629c03f6b4808080808080a0262837b33bb3da27310106971afb7357fcfc81d03b2eece699a870d76b1b91e580a01bbb9a83440fb502d00d6acf19c6e3d516593368ebac7f450391a10928ba6f2180a055feb1b29e943c6f7db21b7627db277f6b83e01059a6654a6ebdf3254240240a8080',
'0xf8669d361205340a5d2cf6c83d3ffe50845ad7f77e7e9b4b4652a27d256a9652b846f8440180a0c75c41d51819b02cbd93d3056287c48245a7c03ee65df0dc57d8402c059508dda0a76fb5bd57a1fd7396db24613584768d39a648acc4b6c20140f076497727fe27'
],
balance: '0x0',
codeHash: '0xa76fb5bd57a1fd7396db24613584768d39a648acc4b6c20140f076497727fe27',
nonce: '0x1',
storageHash: '0xc75c41d51819b02cbd93d3056287c48245a7c03ee65df0dc57d8402c059508dd',
storageProof: [
{
key: '0x1471eb6eb2c5e789fc3de43f8ce62938c7d1836ec861730447e2ada8fd81017b',
value: '0x6443b458eba622ca28cb0334e624d5a48f8380d8d897fcc2',
proof: [
'0xf901f1a0d78ecf6bd70c9e6d9bee814ecef053b7dbe4fc8c4c2a930991a22ef7767ffa9da0325c424caa1de4ca240be4ab514a2d29ea31e8c54b14f3d147290818c6e8a62fa04b1ae38c1509b502ad641ce526d80eeec3a282782e5ecedb0ba3156020e9aa7ea0430a765b6be8c4333c8e566ffca4583fbc6bdc389e21d9577c1cd8556884c9dfa0496156aa8bb763859517ecd1d20de1d288bc2d883bfb0e743b021c8d4cc16001a05fc932af720a07434c792d364178e4f65f0c3ff701809af6164f0d5d309e6d73a078885e55c981340022aeba2f83711c05cbe0c607edb9a402b572724287909437a033c73490c233fbf685588bce5f50b47535299cd1209e698ef41233b0da4827aea0ea85c28c4c83c0840c8e59409cd7db04c3092f8f3d7b7eee1fa5e8f563aed67ca02663779d05f7ad0bab0ab4fb425b543baae9d88d0520c73dbbd9b7a129f71b23a09f9778b67b0aacf01086789ed7792b28cdbf1ffdcab86712e1d0c8fa512395dca09a531cf180b396eb989e999ec898780e70fd39558d983c717410a333e75a9e70a08e5d8a19403e16418c25f7cd2c532d9efdc300427cb7ce9fec79b0128625d286a0747b24aa428c38c8834ffc26d9eff768df2717fa3b9220a8d1b26e5b8b9be159a03d4b481399ac03d21f61fb758fb72c2bfc20022f3bd96113616a4037b84b1c6f8080',
'0xf85180a068385c33ee913dd90e5e2125cf0549f259f4c7e793de7e4e62d8e0529658c3238080808080808080a0dddedcffdbdf97746f81a0004d3799abc634c64ae8b301b8dc1cdabe599e5963808080808080',
'0xf83ba020b3c568e6b9e23c87101e15642000038e2a634c0eba9355f868407d119483c299986443b458eba622ca28cb0334e624d5a48f8380d8d897fcc2'
]
}
]
}
Base on 1-4) and 1-5) step, we can calculate specific variable state storage slot on contract and get proof using
ethGetProof
method. Also need to format it but this will be handled in each network sections page. // 1-4) Calculate storage slot
const proofOwnership = await calculateStorageSlot(address, token_id, contract_data.address, contract_data.proof_blocknumber, contract_data.mapping_storage_slot)
if (!proofOwnership) return res.status(500).json([])
// 1-5) Use ethGetProof and format the proof for each chain compatible
const output = await getProofFromSlot(contract_data.address, proofOwnership?.slot, proofOwnership?.blockNum)
This is the full main function for generate proofs that all you want to verify specific contract variable in other chain using Herodotus.
let contract_data = {
address: '0xB6920Bc97984b454A2A76fE1Be5e099f461Ed9c8',
mapping_storage_slot: 5,
proof_blocknumber: 0
}
const post = async (req: NextApiRequest, res: NextApiResponse<any>) => {
const address = req.body.addr as string
const token_id = req.body.tokenId as number
// get block number
contract_data.proof_blocknumber = await getCurrentBlockNum()
// !------START FROM HERE, YOU CAN REUSE THIS LOGIC TO ANY YOUR PROJECT----------!
// 1-4) Calculate storage slot
const proofOwnership = await calculateStorageSlot(address, token_id, contract_data.address, contract_data.proof_blocknumber, contract_data.mapping_storage_slot)
if (!proofOwnership) return res.status(500).json([])
// 1-5) get ethGetProof and format the proof for each chain compatible
const output = await getProofFromSlot(contract_data.address, proofOwnership?.slot, proofOwnership?.blockNum)
// Call Herodotus API : generate check user's proof and validate it send it to factregistery
await herodotusProof(contract_data.address, proofOwnership?.blockNum )
// !------END HERE, YOU CAN REUSE THIS LOGIC TO ANY YOUR PROJECT----------!
const calldata = [token_id,proofOwnership?.blockNum,address, output.slot, output.proof_sizes_bytes, output.proof_sizes_words, output.proofs_concat ]
const result = {calldata, block_number: proofOwnership?.blockNum}
return res.status(200).json(result)
Expected Question :
Answer :
You need to provide two proofs to prove a variable from a specific storage slot:
- 1.Make sure that this proof about the block is included in the Merkle tree -
MMR Merkle proofs
. - 2.Make sure that the specific variable proof from this storage slot is correct -
eth_getProof
.
If you use the Herodotus API, the API will handle the first part for you. After the API called the webhook and the task is proven, then you can safely proceed to step 2).
Then you need to provide the correct slot as the next step if you want proof for a specific variable of the contract.
Last modified 3mo ago