diff --git a/docs/docs/concepts/foundation/communication/cross_chain_calls.md b/docs/docs/concepts/foundation/communication/cross_chain_calls.md index 86554a820901..abd37f84fcfa 100644 --- a/docs/docs/concepts/foundation/communication/cross_chain_calls.md +++ b/docs/docs/concepts/foundation/communication/cross_chain_calls.md @@ -96,7 +96,7 @@ For the sake of cross-chain messages, this means inserting and nullifying L1 $\r ### Messages -While a message could theoretically be arbitrary long, we want to limit the cost of the insertion on L1 as much as possible. Therefore, we allow the users to send 32 bytes of "content" between L1 and L2. If 32 suffices, no packing required. If the 32 is too "small" for the message directly, the sender should simply pass along a `sha256(content)` instead of the content directly. The content can then either be emitted as an event on L2 or kept by the sender, who should then be the only entity that can "unpack" the message. +While a message could theoretically be arbitrary long, we want to limit the cost of the insertion on L1 as much as possible. Therefore, we allow the users to send 32 bytes of "content" between L1 and L2. If 32 suffices, no packing required. If the 32 is too "small" for the message directly, the sender should simply pass along a `sha256(content)` instead of the content directly (note that this hash should fit in a field element which is ~254 bits. More info on this below). The content can then either be emitted as an event on L2 or kept by the sender, who should then be the only entity that can "unpack" the message. In this manner, there is some way to "unpack" the content on the receiving domain. The message that is passed along, require the `sender/recipient` pair to be communicated as well (we need to know who should receive the message and be able to check). By having the pending messages be a contract on L1, we can ensure that the `sender = msg.sender` and let only `content` and `recipient` be provided by the caller. Summing up, we can use the struct's seen below, and only store the commitment (`sha256(LxToLyMsg)`) on chain or in the trees, this way, we need only update a single storage slot per message. diff --git a/docs/docs/dev_docs/contracts/portals/main.md b/docs/docs/dev_docs/contracts/portals/main.md index 0ff0bc24d402..471eca081fbf 100644 --- a/docs/docs/dev_docs/contracts/portals/main.md +++ b/docs/docs/dev_docs/contracts/portals/main.md @@ -47,6 +47,19 @@ Computing the `content` must be done manually in its current form, as we are sti #include_code claim_public /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr rust +:::info +The `content_hash` is a sha256 truncated to a field element (~ 254 bits). Same is true for the secretHash that gets passed on L1 +In Aztec-nr, you can use our `sha256_to_field()` to do a sha256 hash which fits in one field element: + +#include_code mint_public_content_hash_nr /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr rust + +In solidity, you can use our `Hash.sha256ToField()` method: + +#include_code content_hash_sol_import l1-contracts/test/portals/TokenPortal.sol solidity + +#include_code deposit_public l1-contracts/test/portals/TokenPortal.sol solidity +::: + After the transaction has been mined, the message is consumed, a nullifier is emitted and the tokens have been minted on Aztec and are ready for claiming. Since the message consumption is emitting a nullifier the same message cannot be consumed again. The index in the message tree is used as part of the nullifier computation, ensuring that the same content and secret being inserted will be distinct messages that can each be consumed. Without the index in the nullifier, it would be possible to perform a kind of attack known as `Faerie Gold` attacks where two seemingly good messages are inserted, but only one of them can be consumed later. diff --git a/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md b/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md index 9155fd48be1f..d91c0b3db30e 100644 --- a/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md +++ b/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md @@ -28,16 +28,12 @@ Last, copy-paste the code from the `Token` contract into `contracts/token/main.n #include_code token_all yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr rust -The `Token` contract also requires two helper files. Copy-them too: +The `Token` contract also requires a helper file. Copy it too: Create `contracts/token/types.nr` and copy-paste the following: #include_code token_types_all yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr rust -Finally, create `contracts/token/util.nr` and copy-paste the following: - -#include_code token_util_all yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr rust - ## Compile your contract We'll now use the [Aztec CLI](../../cli/main.md) to [compile](../../contracts/compiling.md) our project. If you haven't installed the CLI already, you can install it locally to your project running: diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index 37d5f5cb472e..ed04aa8dd600 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -7,7 +7,9 @@ import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {IRegistry} from "@aztec/core/interfaces/messagebridge/IRegistry.sol"; import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +// docs:start:content_hash_sol_import import {Hash} from "@aztec/core/libraries/Hash.sol"; +// docs:end:content_hash_sol_import contract TokenPortal { using SafeERC20 for IERC20; diff --git a/yarn-project/aztec-nr/aztec/src/hash.nr b/yarn-project/aztec-nr/aztec/src/hash.nr new file mode 100644 index 000000000000..09bdd46f0d3d --- /dev/null +++ b/yarn-project/aztec-nr/aztec/src/hash.nr @@ -0,0 +1,36 @@ +use dep::std::hash::{pedersen_with_separator, sha256}; +use crate::constants_gen::{ + GENERATOR_INDEX__SIGNATURE_PAYLOAD, + GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET, +}; + +fn sha256_to_field(bytes_to_hash: [u8; N]) -> Field { + let sha256_hashed = sha256(bytes_to_hash); + + // Convert it to a field element + let mut v = 1; + let mut high = 0 as Field; + let mut low = 0 as Field; + + for i in 0..16 { + high = high + (sha256_hashed[15 - i] as Field) * v; + low = low + (sha256_hashed[16 + 15 - i] as Field) * v; + v = v * 256; + } + + // Abuse that a % p + b % p = (a + b) % p and that low < p + let hash_in_a_field = low + high * v; + + hash_in_a_field +} + +fn compute_message_hash(args: [Field; N]) -> Field { + // @todo @lherskind We should probably use a separate generator for this, + // to avoid any potential collisions with payloads. + pedersen_with_separator(args, GENERATOR_INDEX__SIGNATURE_PAYLOAD)[0] +} + +fn compute_secret_hash(secret: Field) -> Field { + // TODO(#1205) This is probably not the right index to use + pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] +} \ No newline at end of file diff --git a/yarn-project/aztec-nr/aztec/src/lib.nr b/yarn-project/aztec-nr/aztec/src/lib.nr index 72ebcd052f88..98c8e638bb01 100644 --- a/yarn-project/aztec-nr/aztec/src/lib.nr +++ b/yarn-project/aztec-nr/aztec/src/lib.nr @@ -5,6 +5,7 @@ mod auth; mod constants_gen; mod context; mod entrypoint; +mod hash; mod log; mod messaging; mod note; diff --git a/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr b/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr index 70e5f6efc239..8ac1a9324908 100644 --- a/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr +++ b/yarn-project/aztec-nr/aztec/src/messaging/l1_to_l2_message.nr @@ -3,6 +3,7 @@ use crate::constants_gen::{ GENERATOR_INDEX__NULLIFIER, GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET, }; +use crate::hash::{sha256_to_field}; struct L1ToL2Message { sender: Field, @@ -64,20 +65,7 @@ impl L1ToL2Message { hash_bytes[i + 224] = fee_bytes[i]; } - let message_sha256 = dep::std::hash::sha256(hash_bytes); - - // Convert the message_sha256 to a field element - let mut v = 1; - let mut high = 0 as Field; - let mut low = 0 as Field; - - for i in 0..16 { - high = high + (message_sha256[15 - i] as Field) * v; - low = low + (message_sha256[16 + 15 - i] as Field) * v; - v = v * 256; - } - - let message_hash = low + high * v; + let message_hash = sha256_to_field(hash_bytes); message_hash } diff --git a/yarn-project/noir-contracts/src/contracts/ecdsa_account_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/ecdsa_account_contract/src/main.nr index be516af65a3d..d9ddd5928012 100644 --- a/yarn-project/noir-contracts/src/contracts/ecdsa_account_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/ecdsa_account_contract/src/main.nr @@ -7,7 +7,6 @@ contract EcdsaAccount { use dep::std::option::Option; use dep::aztec::{ abi::CallContext, - constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD, context::{PrivateContext, PublicContext, Context}, entrypoint::{EntrypointPayload, ENTRYPOINT_PAYLOAD_SIZE}, log::emit_encrypted_log, diff --git a/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr index ad591369e082..562e403d1f78 100644 --- a/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr @@ -6,7 +6,6 @@ contract SchnorrHardcodedAccount { entrypoint::{ EntrypointPayload, ENTRYPOINT_PAYLOAD_SIZE }, abi::{ PrivateCircuitPublicInputs, PrivateContextInputs, Hasher }, types::{ vec::BoundedVec, point::Point }, - constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD, context::PrivateContext, account::AccountActions, oracle::auth_witness::get_auth_witness, diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr index 88b854ed1e33..ac5563fa8353 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -9,6 +9,7 @@ mod token_interface; contract TokenBridge { use dep::aztec::{ context::{Context}, + hash::{compute_secret_hash}, state_vars::{public_state::PublicState}, types::type_serialization::field_serialization::{ FieldSerializationMethods, FIELD_SERIALIZED_LEN, @@ -18,7 +19,7 @@ contract TokenBridge { }; use crate::token_interface::Token; - use crate::util::{get_mint_public_content_hash, get_mint_private_content_hash, get_withdraw_content_hash, compute_secret_hash}; + use crate::util::{get_mint_public_content_hash, get_mint_private_content_hash, get_withdraw_content_hash}; // Storage structure, containing all storage, and specifying what slots they use. struct Storage { diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr index 7ecb7d547cd8..0cce89486d20 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/util.nr @@ -1,87 +1,54 @@ -use dep::std::hash::{pedersen_with_separator, sha256}; -use dep::aztec::constants_gen::{ - GENERATOR_INDEX__SIGNATURE_PAYLOAD, - GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET, -}; +use dep::std::hash::pedersen_with_separator; +// docs:start:mint_public_content_hash_nr +use dep::aztec::hash::{to_bytes_with_selector, sha256_to_field}; -fn compute_secret_hash(secret: Field) -> Field { - // TODO(#1205) This is probably not the right index to use - pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] -} - -// Computes a content hash of a deposit/mint_private message. +// Computes a content hash of a deposit/mint_public message. // Refer TokenPortal.sol for reference on L1. -fn get_mint_private_content_hash(amount: Field, secret_hash_for_redeeming_minted_notes: Field, canceller: Field) -> Field { +fn get_mint_public_content_hash(owner_address: Field, amount: Field, canceller: Field) -> Field { + let mut hash_bytes: [u8; 100] = [0; 100]; let amount_bytes = amount.to_be_bytes(32); - let secret_hash_bytes = secret_hash_for_redeeming_minted_notes.to_be_bytes(32); + let recipient_bytes = owner_address.to_be_bytes(32); let canceller_bytes = canceller.to_be_bytes(32); for i in 0..32 { hash_bytes[i + 4] = amount_bytes[i]; - hash_bytes[i + 36] = secret_hash_bytes[i]; + hash_bytes[i + 36] = recipient_bytes[i]; hash_bytes[i + 68] = canceller_bytes[i]; } - // Function selector: 0x25d46b0f keccak256('mint_private(uint256,bytes32,address)') - hash_bytes[0] = 0x25; - hash_bytes[1] = 0xd4; - hash_bytes[2] = 0x6b; - hash_bytes[3] = 0x0f; - - let content_sha256 = sha256(hash_bytes); - - // // Convert the content_sha256 to a field element - let mut v = 1; - let mut high = 0 as Field; - let mut low = 0 as Field; - - for i in 0..16 { - high = high + (content_sha256[15 - i] as Field) * v; - low = low + (content_sha256[16 + 15 - i] as Field) * v; - v = v * 256; - } + // Function selector: 0x63c9440d keccak256('mint_public(uint256,bytes32,address)') + hash_bytes[0] = 0x63; + hash_bytes[1] = 0xc9; + hash_bytes[2] = 0x44; + hash_bytes[3] = 0x0d; - // Abuse that a % p + b % p = (a + b) % p and that low < p - let content_hash = low + high * v; + let content_hash = sha256_to_field(hash_bytes); content_hash } +// docs:end:mint_public_content_hash_nr -// Computes a content hash of a deposit/mint_public message. +// Computes a content hash of a deposit/mint_private message. // Refer TokenPortal.sol for reference on L1. -fn get_mint_public_content_hash(owner_address: Field, amount: Field, canceller: Field) -> Field { +fn get_mint_private_content_hash(amount: Field, secret_hash_for_redeeming_minted_notes: Field, canceller: Field) -> Field { let mut hash_bytes: [u8; 100] = [0; 100]; let amount_bytes = amount.to_be_bytes(32); - let recipient_bytes = owner_address.to_be_bytes(32); + let secret_hash_bytes = secret_hash_for_redeeming_minted_notes.to_be_bytes(32); let canceller_bytes = canceller.to_be_bytes(32); for i in 0..32 { hash_bytes[i + 4] = amount_bytes[i]; - hash_bytes[i + 36] = recipient_bytes[i]; + hash_bytes[i + 36] = secret_hash_bytes[i]; hash_bytes[i + 68] = canceller_bytes[i]; } - // Function selector: 0x63c9440d keccak256('mint_public(uint256,bytes32,address)') - hash_bytes[0] = 0x63; - hash_bytes[1] = 0xc9; - hash_bytes[2] = 0x44; - hash_bytes[3] = 0x0d; - - let content_sha256 = sha256(hash_bytes); - - // // Convert the content_sha256 to a field element - let mut v = 1; - let mut high = 0 as Field; - let mut low = 0 as Field; - - for i in 0..16 { - high = high + (content_sha256[15 - i] as Field) * v; - low = low + (content_sha256[16 + 15 - i] as Field) * v; - v = v * 256; - } + // Function selector: 0x25d46b0f keccak256('mint_private(uint256,bytes32,address)') + hash_bytes[0] = 0x25; + hash_bytes[1] = 0xd4; + hash_bytes[2] = 0x6b; + hash_bytes[3] = 0x0f; - // Abuse that a % p + b % p = (a + b) % p and that low < p - let content_hash = low + high * v; + let content_hash = sha256_to_field(hash_bytes); content_hash } @@ -107,20 +74,6 @@ fn get_withdraw_content_hash(recipient: Field, amount: Field, callerOnL1: Field) hash_bytes[i + 36] = recipient_bytes[i]; hash_bytes[i + 68] = callerOnL1_bytes[i]; } - let content_sha256 = sha256(hash_bytes); - - // Convert the content_sha256 to a field element - let mut v = 1; - let mut high = 0 as Field; - let mut low = 0 as Field; - - for i in 0..16 { - high = high + (content_sha256[15 - i] as Field) * v; - low = low + (content_sha256[16 + 15 - i] as Field) * v; - v = v * 256; - } - - // Abuse that a % p + b % p = (a + b) % p and that low < p - let content = low + high * v; - content + let content_hash = sha256_to_field(hash_bytes); + content_hash } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr index 2cf48a529945..3d8cc7e1b443 100644 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr @@ -1,7 +1,6 @@ // docs:start:token_all // docs:start:imports mod types; -mod util; // Minimal token implementation that supports `AuthWit` accounts. // The auth message follows a similar pattern to the cross-chain message and includes a designated caller. @@ -22,6 +21,7 @@ contract Token { utils as note_utils, }, context::{PrivateContext, PublicContext, Context}, + hash::{compute_message_hash}, state_vars::{map::Map, public_state::PublicState, set::Set}, types::type_serialization::{ field_serialization::{FieldSerializationMethods, FIELD_SERIALIZED_LEN}, @@ -39,7 +39,6 @@ contract Token { balances_map::{BalancesMap}, safe_u120_serialization::{SafeU120SerializationMethods, SAFE_U120_SERIALIZED_LEN} }; - use crate::util::{compute_message_hash}; // docs:end::imports // docs:start:storage_struct @@ -454,4 +453,4 @@ contract Token { // docs:end:compute_note_hash_and_nullifier } -// docs:end:token_all +// docs:end:token_all \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr index ae32796f2bde..bce35b246601 100644 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr @@ -1,12 +1,11 @@ // docs:start:token_types_all use dep::std::hash::pedersen; -use dep::std::hash::pedersen_with_separator; use dep::aztec::note::{ note_header::NoteHeader, note_interface::NoteInterface, utils::compute_siloed_note_hash, }; -use dep::aztec::constants_gen::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET; +use dep::aztec::hash::{compute_secret_hash}; global TRANSPARENT_NOTE_LEN: Field = 2; @@ -39,7 +38,7 @@ impl TransparentNote { fn new_from_secret(amount: Field, secret: Field) -> Self { TransparentNote { amount: amount, - secret_hash: TransparentNote::compute_secret_hash(secret), + secret_hash: compute_secret_hash(secret), secret: secret, header: NoteHeader::empty(), } @@ -83,13 +82,8 @@ impl TransparentNote { // CUSTOM FUNCTIONS FOR THIS NOTE TYPE - fn compute_secret_hash(secret: Field) -> Field { - // TODO(#1205) This is probably not the right index to use - pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] - } - fn knows_secret(self, secret: Field) { - let hash = TransparentNote::compute_secret_hash(secret); + let hash = compute_secret_hash(secret); assert(self.secret_hash == hash); } } diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr deleted file mode 100644 index 701d0dd05278..000000000000 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/util.nr +++ /dev/null @@ -1,10 +0,0 @@ -// docs:start:token_util_all -use dep::std::hash::{pedersen_with_separator}; -use dep::aztec::constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD; - -fn compute_message_hash(args: [Field; N]) -> Field { - // @todo @lherskind We should probably use a separate generator for this, - // to avoid any potential collisions with payloads. - pedersen_with_separator(args, GENERATOR_INDEX__SIGNATURE_PAYLOAD)[0] -} -// docs:end:token_util_all \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr index ccca6825078a..f5ac87f5ca65 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr @@ -8,6 +8,7 @@ contract Uniswap { use dep::aztec::{ auth::IS_VALID_SELECTOR, context::{PrivateContext, PublicContext, Context}, + hash::compute_message_hash, oracle::compute_selector::compute_selector, oracle::context::get_portal_address, state_vars::{map::Map, public_state::PublicState}, @@ -21,7 +22,7 @@ contract Uniswap { }; use crate::interfaces::{Token, TokenBridge}; - use crate::util::{compute_message_hash, compute_swap_private_content_hash}; + use crate::util::compute_swap_private_content_hash; struct Storage { // like with account contracts, stores the approval message on a slot and tracks if they are active diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr index da1361ab8e34..8c16eb0b995c 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr @@ -1,11 +1,4 @@ -use dep::std::hash::{pedersen_with_separator, sha256}; -use dep::aztec::constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD; - -fn compute_message_hash(args: [Field; N]) -> Field { - // @todo @lherskind We should probably use a separate generator for this, - // to avoid any potential collisions with payloads. - pedersen_with_separator(args, GENERATOR_INDEX__SIGNATURE_PAYLOAD)[0] -} +use dep::aztec::hash::sha256_to_field; // This method computes the L2 to L1 message content hash for the private // refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected @@ -52,22 +45,6 @@ fn compute_swap_private_content_hash( hash_bytes[i + 260] = canceller_bytes[i]; hash_bytes[i + 292] = caller_on_L1_bytes[i]; } - - let content_sha256 = sha256(hash_bytes); - - // Convert the content_sha256 to a field element - let mut v = 1; - let mut high = 0 as Field; - let mut low = 0 as Field; - - for i in 0..16 { - high = high + (content_sha256[15 - i] as Field) * v; - low = low + (content_sha256[16 + 15 - i] as Field) * v; - v = v * 256; - } - - // Abuse that a % p + b % p = (a + b) % p and that low < p - let content_hash = low + high * v; - + let content_hash = sha256_to_field(hash_bytes); content_hash }