智能合约优化困惑:Hitchiker's Guide中迭代校验的Gas成本问题
Great question—this is a super common pain point when building notarization-style smart contracts, so let’s break this down clearly.
Why Iterating All Proofs Causes Rising Gas Costs
First, let’s recall how Ethereum gas works: every operation your contract executes (reading/writing storage, looping, arithmetic) has a fixed gas cost attached. When you iterate through an array of proofs to check if a document is notarized, you’re reading a storage slot for every single proof in the array each time you run the check.
Note that every time we want to check if a document was notarized, we need to iterate through all existing proofs. This makes the contract spend more and more gas on each check as more documents are added.
As the quote says, if you start with 10 proofs, your check reads 10 storage slots. When you hit 1,000 proofs, it reads 1,000 slots—gas cost grows linearly with the number of documents. For users interacting with your contract, this means check operations get more expensive over time, which is terrible for UX and scalability.
The Fix: Use Mappings for O(1) Lookups
The simplest and most effective fix is to replace array-based proof storage with a mapping that directly indexes documents by their hash. Mappings in Solidity let you look up values in constant time (O(1)) regardless of how many entries exist—no looping required.
Here’s a simplified example of an optimized notary contract:
contract OptimizedNotary { // Map document hashes to a boolean indicating if they're notarized mapping(bytes32 => bool) public isNotarized; function notarize(bytes32 documentHash) external { require(!isNotarized[documentHash], "Document already notarized"); isNotarized[documentHash] = true; } function checkNotarized(bytes32 documentHash) external view returns (bool) { // Direct lookup—no iteration, fixed gas cost every time return isNotarized[documentHash]; } }
In this setup, checkNotarized will cost the same amount of gas whether you’ve notarized 10 documents or 10,000. No more linear gas growth.
If You Need to Track Historical Proofs
If you still need a full record of all notarized documents (e.g., for frontend dashboards or audit trails), you can combine a mapping with an array:
contract OrderedNotary { struct Proof { bytes32 documentHash; uint256 timestamp; address notarizer; } mapping(bytes32 => bool) public isNotarized; Proof[] public allProofs; // Stores historical proofs for reference function notarize(bytes32 documentHash) external { require(!isNotarized[documentHash], "Document already notarized"); isNotarized[documentHash] = true; allProofs.push(Proof(documentHash, block.timestamp, msg.sender)); } function checkNotarized(bytes32 documentHash) external view returns (bool) { // Still O(1) lookup—no iteration here return isNotarized[documentHash]; } // Optional: Fetch all proofs (only use this when necessary, as it costs gas proportional to array length) function getAllProofs() external view returns (Proof[] memory) { return allProofs; } }
This way, your core check operation stays fast and cheap, while you still have access to the full history for use cases that need it.
Key Takeaway
The root issue is that linear iteration over storage scales poorly. By using mappings to index state directly, you eliminate the need for iteration in critical check operations, keeping gas costs predictable and scalable as your contract grows.
内容的提问来源于stack exchange,提问作者Mehmet Doğan

