11#include " barretenberg/vm2/simulation/gadgets/update_check.hpp"
22
3- #include " barretenberg/vm2/common/aztec_constants.hpp"
43#include " barretenberg/vm2/common/constants.hpp"
54#include " barretenberg/vm2/common/stringify.hpp"
65#include " barretenberg/vm2/simulation/lib/merkle.hpp"
@@ -9,8 +8,12 @@ namespace bb::avm2::simulation {
98
109namespace {
1110
12- using UnconstrainedPoseidon2 = crypto::Poseidon2<crypto::Poseidon2Bn254ScalarFieldParams>;
13-
11+ /* *
12+ * @brief Read a leaf value from the public data tree at a given slot without emitting constrained events.
13+ * @param merkle_db The low-level merkle database interface for unconstrained reads.
14+ * @param leaf_slot The slot to read from in the public data tree.
15+ * @return The leaf value if present, 0 otherwise.
16+ */
1417FF unconstrained_read (const LowLevelMerkleDBInterface& merkle_db, const FF & leaf_slot)
1518{
1619 auto [present, index] = merkle_db.get_low_indexed_leaf (world_state::MerkleTreeId::PUBLIC_DATA_TREE , leaf_slot);
@@ -20,6 +23,24 @@ FF unconstrained_read(const LowLevelMerkleDBInterface& merkle_db, const FF& leaf
2023
2124} // namespace
2225
26+ /* *
27+ * @brief Validate that a contract's current class ID is consistent with the delayed public mutable update state.
28+ *
29+ * Reads the delayed public mutable hash from the public data tree. If the hash is zero, the contract was never
30+ * updated and current_class_id must equal original_class_id. Otherwise, reads the preimage (metadata, pre, post)
31+ * in unconstrained mode, verifies it against the hash, decomposes the metadata to extract the timestamp of change,
32+ * and selects the pre or post class ID based on whether the update has taken effect yet.
33+ *
34+ * Emits a single UpdateCheckEvent on success with all fields populated. When the hash is zero, the preimage
35+ * fields (metadata, pre_class_id, post_class_id) are all zero in the emitted event.
36+ *
37+ * @param address The contract address to check.
38+ * @param instance The contract instance containing original and current class IDs to validate.
39+ * @throws std::runtime_error If hash is zero and current_class_id does not match original_class_id.
40+ * @throws std::runtime_error If the stored hash does not match the reconstructed preimage hash (sanity check).
41+ * @throws std::runtime_error If the expected class ID (derived from update state and timestamp) does not match
42+ * current_class_id.
43+ */
2344void UpdateCheck::check_current_class_id (const AztecAddress& address, const ContractInstance& instance)
2445{
2546 // Compute the public data tree slots
@@ -30,22 +51,28 @@ void UpdateCheck::check_current_class_id(const AztecAddress& address, const Cont
3051 // where we store in one public data tree slot the hash of the whole structure. This is nice because in circuits you
3152 // can receive the preimage as a hint and just read 1 storage slot instead of 3. We do that here, we will constrain
3253 // the hash read but then read in unconstrained mode the preimage. The PIL for this gadget constrains the hash.
33- FF hash = merkle_db.storage_read (CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS , delayed_public_mutable_hash_slot);
54+ FF update_hash =
55+ merkle_db.storage_read (CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS , delayed_public_mutable_hash_slot);
3456
3557 uint256_t update_preimage_metadata = 0 ;
3658 FF update_preimage_pre_class_id = 0 ;
3759 FF update_preimage_post_class_id = 0 ;
3860
3961 uint64_t current_timestamp = globals.timestamp ;
4062
41- if (hash == 0 ) {
63+ if (update_hash == 0 ) {
4264 // If the delayed public mutable has never been written, then the contract was never updated. We short circuit
4365 // early.
4466 if (instance.original_contract_class_id != instance.current_contract_class_id ) {
4567 throw std::runtime_error (" Current class id does not match expected class id" );
4668 }
4769 } else {
48- // Read the preimage from the tree in unconstrained mode
70+ // The preimage (metadata, pre_class_id, post_class_id) is read in unconstrained mode because
71+ // the PIL constrains hash(metadata, pre, post) == stored_hash, making a single constrained
72+ // hash read sufficient. The preimage slots are guaranteed to be consistent with the hash:
73+ // all writes go through DelayedPublicMutable::write(), which atomically writes all 4 slots
74+ // (3 preimage + hash) in a single storage_write call, and storage is siloed to the
75+ // ContractInstanceRegistry contract so no external contract/actor can tamper with these slots.
4976 LowLevelMerkleDBInterface& unconstrained_merkle_db = merkle_db.as_unconstrained ();
5077
5178 std::vector<FF > update_preimage (3 );
@@ -59,7 +86,7 @@ void UpdateCheck::check_current_class_id(const AztecAddress& address, const Cont
5986 // Double check that the unconstrained reads match the hash. This is just a sanity check, if slow, can be
6087 // removed.
6188 FF reconstructed_hash = poseidon2.hash (update_preimage);
62- if (hash != reconstructed_hash) {
89+ if (update_hash != reconstructed_hash) {
6390 throw std::runtime_error (" Stored hash does not match preimage hash" );
6491 }
6592
@@ -73,11 +100,14 @@ void UpdateCheck::check_current_class_id(const AztecAddress& address, const Cont
73100 // bit timestamp being packed in 32 bits is a tech debt that is not worth tackling.
74101 uint64_t timestamp_of_change =
75102 static_cast <uint64_t >(static_cast <uint32_t >(update_preimage_metadata & 0xffffffff ));
103+ // Constrain that these items fit in their respective bit-sizes so that malicious prover
104+ // cannot provide larger values.
76105 range_check.assert_range (update_metadata_hi,
77106 UPDATES_DELAYED_PUBLIC_MUTABLE_METADATA_BIT_SIZE - TIMESTAMP_OF_CHANGE_BIT_SIZE );
78107 range_check.assert_range (timestamp_of_change, TIMESTAMP_OF_CHANGE_BIT_SIZE );
79108
80- // pre and post can be zero, if they have never been touched. In that case we need to use the original class id.
109+ // pre and post can be zero, if they have never been touched (or have been reset). In that case we need to use
110+ // the original class id.
81111 FF pre_class =
82112 update_preimage_pre_class_id == 0 ? instance.original_contract_class_id : update_preimage_pre_class_id;
83113 FF post_class =
@@ -98,7 +128,7 @@ void UpdateCheck::check_current_class_id(const AztecAddress& address, const Cont
98128 .original_class_id = instance.original_contract_class_id ,
99129 .public_data_tree_root = merkle_db.get_tree_state ().public_data_tree .tree .root ,
100130 .current_timestamp = current_timestamp,
101- .update_hash = hash ,
131+ .update_hash = update_hash ,
102132 .update_preimage_metadata = update_preimage_metadata,
103133 .update_preimage_pre_class_id = update_preimage_pre_class_id,
104134 .update_preimage_post_class_id = update_preimage_post_class_id,
0 commit comments