From c592fd36cab3145e007df56280f4a94f0cb9fa62 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Mon, 4 Dec 2023 01:25:00 -0800 Subject: [PATCH] [fortuna] Add confirmation delay before responding (#1161) * Update abi * fix tests * update contract * fix fortuna * test confirmation delay * cleanup * fix * split the fields * saturating sub * rename --- fortuna/config.yaml | 4 + fortuna/src/abi.json | 151 +++++++++++------- fortuna/src/api.rs | 129 ++++++++++++--- fortuna/src/api/revelation.rs | 20 ++- fortuna/src/chain/ethereum.rs | 11 +- fortuna/src/chain/reader.rs | 42 ++++- fortuna/src/command/run.rs | 1 + fortuna/src/config.rs | 8 +- fortuna/src/config/register_provider.rs | 3 +- .../contracts/contracts/entropy/Entropy.sol | 9 +- .../contracts/forge-test/Entropy.t.sol | 18 ++- .../entropy_sdk/solidity/EntropyStructs.sol | 12 +- 12 files changed, 307 insertions(+), 101 deletions(-) diff --git a/fortuna/config.yaml b/fortuna/config.yaml index 62991b21de..021162d03f 100644 --- a/fortuna/config.yaml +++ b/fortuna/config.yaml @@ -2,13 +2,17 @@ chains: optimism-goerli: geth_rpc_addr: https://goerli.optimism.io contract_addr: 0x28F16Af4D87523910b843a801454AEde5F9B0459 + reveal_delay_blocks: 0 avalanche-fuji: geth_rpc_addr: https://api.avax-test.network/ext/bc/C/rpc contract_addr: 0xD42c7a708E74AD19401D907a14146F006c851Ee3 + reveal_delay_blocks: 0 eos-evm-testnet: geth_rpc_addr: https://api.testnet.evm.eosnetwork.com/ contract_addr: 0xD42c7a708E74AD19401D907a14146F006c851Ee3 + reveal_delay_blocks: 0 legacy_tx: true arbitrum-goerli: geth_rpc_addr: https://arbitrum-goerli.publicnode.com contract_addr: 0xd9eAcfFB8e80b7193042499485EF8369b08E85B6 + reveal_delay_blocks: 0 diff --git a/fortuna/src/abi.json b/fortuna/src/abi.json index 86175d0366..55d7f8bdca 100644 --- a/fortuna/src/abi.json +++ b/fortuna/src/abi.json @@ -1,19 +1,29 @@ [ { - "type": "constructor", - "inputs": [ + "type": "function", + "name": "NUM_REQUESTS", + "inputs": [], + "outputs": [ { - "name": "pythFeeInWei", - "type": "uint256", - "internalType": "uint256" - }, + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NUM_REQUESTS_MASK", + "inputs": [], + "outputs": [ { - "name": "defaultProvider", - "type": "address", - "internalType": "address" + "name": "", + "type": "bytes1", + "internalType": "bytes1" } ], - "stateMutability": "nonpayable" + "stateMutability": "view" }, { "type": "function", @@ -70,8 +80,8 @@ "outputs": [ { "name": "accruedPythFeesInWei", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "stateMutability": "view" @@ -102,8 +112,8 @@ "outputs": [ { "name": "feeAmount", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "stateMutability": "view" @@ -126,13 +136,13 @@ "components": [ { "name": "feeInWei", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "accruedFeesInWei", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "originalCommitment", @@ -179,6 +189,19 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getPythFee", + "inputs": [], + "outputs": [ + { + "name": "feeAmount", + "type": "uint128", + "internalType": "uint128" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getRequest", @@ -211,24 +234,29 @@ "internalType": "uint64" }, { - "name": "userCommitment", - "type": "bytes32", - "internalType": "bytes32" + "name": "numHashes", + "type": "uint32", + "internalType": "uint32" }, { - "name": "providerCommitment", + "name": "commitment", "type": "bytes32", "internalType": "bytes32" }, { - "name": "providerCommitmentSequenceNumber", + "name": "blockNumber", "type": "uint64", "internalType": "uint64" }, { - "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "name": "requester", + "type": "address", + "internalType": "address" + }, + { + "name": "useBlockhash", + "type": "bool", + "internalType": "bool" } ] } @@ -241,8 +269,8 @@ "inputs": [ { "name": "feeInWei", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "commitment", @@ -337,8 +365,8 @@ "inputs": [ { "name": "amount", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "outputs": [], @@ -356,13 +384,13 @@ "components": [ { "name": "feeInWei", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "accruedFeesInWei", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "originalCommitment", @@ -430,24 +458,29 @@ "internalType": "uint64" }, { - "name": "userCommitment", - "type": "bytes32", - "internalType": "bytes32" + "name": "numHashes", + "type": "uint32", + "internalType": "uint32" }, { - "name": "providerCommitment", + "name": "commitment", "type": "bytes32", "internalType": "bytes32" }, { - "name": "providerCommitmentSequenceNumber", + "name": "blockNumber", "type": "uint64", "internalType": "uint64" }, { - "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "name": "requester", + "type": "address", + "internalType": "address" + }, + { + "name": "useBlockhash", + "type": "bool", + "internalType": "bool" } ] } @@ -475,24 +508,29 @@ "internalType": "uint64" }, { - "name": "userCommitment", - "type": "bytes32", - "internalType": "bytes32" + "name": "numHashes", + "type": "uint32", + "internalType": "uint32" }, { - "name": "providerCommitment", + "name": "commitment", "type": "bytes32", "internalType": "bytes32" }, { - "name": "providerCommitmentSequenceNumber", + "name": "blockNumber", "type": "uint64", "internalType": "uint64" }, { - "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "name": "requester", + "type": "address", + "internalType": "address" + }, + { + "name": "useBlockhash", + "type": "bool", + "internalType": "bool" } ] }, @@ -530,27 +568,32 @@ }, { "type": "error", - "name": "IncorrectProviderRevelation", + "name": "IncorrectRevelation", "inputs": [] }, { "type": "error", - "name": "IncorrectUserRevelation", + "name": "InsufficientFee", "inputs": [] }, { "type": "error", - "name": "InsufficientFee", + "name": "NoSuchProvider", "inputs": [] }, { "type": "error", - "name": "NoSuchProvider", + "name": "NoSuchRequest", "inputs": [] }, { "type": "error", "name": "OutOfRandomness", "inputs": [] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [] } ] diff --git a/fortuna/src/api.rs b/fortuna/src/api.rs index dfe49aa45c..f23d0bd9f5 100644 --- a/fortuna/src/api.rs +++ b/fortuna/src/api.rs @@ -1,6 +1,9 @@ use { crate::{ - chain::reader::EntropyReader, + chain::reader::{ + BlockNumber, + EntropyReader, + }, state::HashChainState, }, axum::{ @@ -68,11 +71,14 @@ impl ApiState { #[derive(Clone)] pub struct BlockchainState { /// The hash chain(s) required to serve random numbers for this blockchain - pub state: Arc, + pub state: Arc, /// The contract that the server is fulfilling requests for. - pub contract: Arc, + pub contract: Arc, /// The address of the provider that this server is operating for. - pub provider_address: Address, + pub provider_address: Address, + /// The server will wait for this many block confirmations of a request before revealing + /// the random number. + pub reveal_delay_blocks: BlockNumber, } pub struct Metrics { @@ -115,6 +121,9 @@ pub enum RestError { /// The caller requested a random value that can't currently be revealed (because it /// hasn't been committed to on-chain) NoPendingRequest, + /// The request exists, but the server is waiting for more confirmations (more blocks + /// to be mined) before revealing the random number. + PendingConfirmation, /// The server cannot currently communicate with the blockchain, so is not able to verify /// which random values have been requested. TemporarilyUnavailable, @@ -136,6 +145,10 @@ impl IntoResponse for RestError { RestError::NoPendingRequest => ( StatusCode::FORBIDDEN, "The random value cannot currently be retrieved", + ).into_response(), + RestError::PendingConfirmation => ( + StatusCode::FORBIDDEN, + "The request needs additional confirmations before the random value can be retrieved. Try your request again later.", ) .into_response(), RestError::TemporarilyUnavailable => ( @@ -210,20 +223,22 @@ mod test { } fn test_server() -> (TestServer, Arc, Arc) { - let eth_read = Arc::new(MockEntropyReader::with_requests(&[])); + let eth_read = Arc::new(MockEntropyReader::with_requests(10, &[])); let eth_state = BlockchainState { - state: ETH_CHAIN.clone(), - contract: eth_read.clone(), - provider_address: PROVIDER, + state: ETH_CHAIN.clone(), + contract: eth_read.clone(), + provider_address: PROVIDER, + reveal_delay_blocks: 1, }; - let avax_read = Arc::new(MockEntropyReader::with_requests(&[])); + let avax_read = Arc::new(MockEntropyReader::with_requests(10, &[])); let avax_state = BlockchainState { - state: AVAX_CHAIN.clone(), - contract: avax_read.clone(), - provider_address: PROVIDER, + state: AVAX_CHAIN.clone(), + contract: avax_read.clone(), + provider_address: PROVIDER, + reveal_delay_blocks: 2, }; let api_state = ApiState::new(&[ @@ -258,7 +273,7 @@ mod test { .await; // Once someone requests the number, then it is accessible - eth_contract.insert(PROVIDER, 0); + eth_contract.insert(PROVIDER, 0, 1, false); let response = get_and_assert_status(&server, "/v1/chains/ethereum/revelations/0", StatusCode::OK) .await; @@ -267,12 +282,12 @@ mod test { }); // Each chain and provider has its own set of requests - eth_contract.insert(PROVIDER, 100); - eth_contract.insert(*OTHER_PROVIDER, 101); - eth_contract.insert(PROVIDER, 102); - avax_contract.insert(PROVIDER, 102); - avax_contract.insert(PROVIDER, 103); - avax_contract.insert(*OTHER_PROVIDER, 104); + eth_contract.insert(PROVIDER, 100, 1, false); + eth_contract.insert(*OTHER_PROVIDER, 101, 1, false); + eth_contract.insert(PROVIDER, 102, 1, false); + avax_contract.insert(PROVIDER, 102, 1, false); + avax_contract.insert(PROVIDER, 103, 1, false); + avax_contract.insert(*OTHER_PROVIDER, 104, 1, false); let response = get_and_assert_status( &server, @@ -365,7 +380,7 @@ mod test { StatusCode::FORBIDDEN, ) .await; - avax_contract.insert(PROVIDER, 99); + avax_contract.insert(PROVIDER, 99, 1, false); get_and_assert_status( &server, "/v1/chains/avalanche/revelations/99", @@ -373,4 +388,78 @@ mod test { ) .await; } + + #[tokio::test] + async fn test_revelation_confirmation_delay() { + let (server, eth_contract, avax_contract) = test_server(); + + eth_contract.insert(PROVIDER, 0, 10, false); + eth_contract.insert(PROVIDER, 1, 11, false); + eth_contract.insert(PROVIDER, 2, 12, false); + + avax_contract.insert(PROVIDER, 100, 10, false); + avax_contract.insert(PROVIDER, 101, 11, false); + + eth_contract.set_block_number(10); + avax_contract.set_block_number(10); + + get_and_assert_status( + &server, + "/v1/chains/ethereum/revelations/0", + StatusCode::FORBIDDEN, + ) + .await; + + get_and_assert_status( + &server, + "/v1/chains/avalanche/revelations/100", + StatusCode::FORBIDDEN, + ) + .await; + + eth_contract.set_block_number(11); + avax_contract.set_block_number(11); + + get_and_assert_status(&server, "/v1/chains/ethereum/revelations/0", StatusCode::OK).await; + + get_and_assert_status( + &server, + "/v1/chains/ethereum/revelations/1", + StatusCode::FORBIDDEN, + ) + .await; + + get_and_assert_status( + &server, + "/v1/chains/avalanche/revelations/100", + StatusCode::FORBIDDEN, + ) + .await; + + eth_contract.set_block_number(12); + avax_contract.set_block_number(12); + + get_and_assert_status(&server, "/v1/chains/ethereum/revelations/1", StatusCode::OK).await; + + get_and_assert_status( + &server, + "/v1/chains/ethereum/revelations/2", + StatusCode::FORBIDDEN, + ) + .await; + + get_and_assert_status( + &server, + "/v1/chains/avalanche/revelations/100", + StatusCode::OK, + ) + .await; + + get_and_assert_status( + &server, + "/v1/chains/avalanche/revelations/101", + StatusCode::FORBIDDEN, + ) + .await; + } } diff --git a/fortuna/src/api/revelation.rs b/fortuna/src/api/revelation.rs index d0745ba2a0..af34946be0 100644 --- a/fortuna/src/api/revelation.rs +++ b/fortuna/src/api/revelation.rs @@ -15,6 +15,7 @@ use { }, pythnet_sdk::wire::array, serde_with::serde_as, + tokio::try_join, utoipa::{ IntoParams, ToSchema, @@ -59,14 +60,20 @@ pub async fn revelation( .get(&chain_id) .ok_or_else(|| RestError::InvalidChainId)?; - let maybe_request = state - .contract - .get_request(state.provider_address, sequence) - .await - .map_err(|_| RestError::TemporarilyUnavailable)?; + let maybe_request_fut = state.contract.get_request(state.provider_address, sequence); + + let current_block_number_fut = state.contract.get_block_number(); + + let (maybe_request, current_block_number) = + try_join!(maybe_request_fut, current_block_number_fut).map_err(|e| { + tracing::error!("RPC request failed {}", e); + RestError::TemporarilyUnavailable + })?; match maybe_request { - Some(_) => { + Some(r) + if current_block_number.saturating_sub(state.reveal_delay_blocks) >= r.block_number => + { let value = &state .state .reveal(sequence) @@ -77,6 +84,7 @@ pub async fn revelation( value: encoded_value, })) } + Some(_) => Err(RestError::PendingConfirmation), None => Err(RestError::NoPendingRequest), } } diff --git a/fortuna/src/chain/ethereum.rs b/fortuna/src/chain/ethereum.rs index 2e4ad93765..395098b95e 100644 --- a/fortuna/src/chain/ethereum.rs +++ b/fortuna/src/chain/ethereum.rs @@ -2,7 +2,10 @@ use { crate::{ chain::{ reader, - reader::EntropyReader, + reader::{ + BlockNumber, + EntropyReader, + }, }, config::EthereumConfig, }, @@ -194,9 +197,15 @@ impl EntropyReader for PythContract { Ok(Some(reader::Request { provider: r.provider, sequence_number: r.sequence_number, + block_number: r.block_number.try_into()?, + use_blockhash: r.use_blockhash, })) } else { Ok(None) } } + + async fn get_block_number(&self) -> Result { + Ok(self.client().get_block_number().await?.as_u64()) + } } diff --git a/fortuna/src/chain/reader.rs b/fortuna/src/chain/reader.rs index bcd5e80083..cf3922df3e 100644 --- a/fortuna/src/chain/reader.rs +++ b/fortuna/src/chain/reader.rs @@ -4,6 +4,8 @@ use { ethers::types::Address, }; +pub type BlockNumber = u64; + /// EntropyReader is the read-only interface of the Entropy contract. #[async_trait] pub trait EntropyReader: Send + Sync { @@ -12,6 +14,8 @@ pub trait EntropyReader: Send + Sync { /// need to become more generic. async fn get_request(&self, provider: Address, sequence_number: u64) -> Result>; + + async fn get_block_number(&self) -> Result; } /// An in-flight request stored in the contract. @@ -21,6 +25,9 @@ pub trait EntropyReader: Send + Sync { pub struct Request { pub provider: Address, pub sequence_number: u64, + // The block number where this request was created + pub block_number: BlockNumber, + pub use_blockhash: bool, } @@ -28,6 +35,7 @@ pub struct Request { pub mod mock { use { crate::chain::reader::{ + BlockNumber, EntropyReader, Request, }, @@ -41,19 +49,26 @@ pub mod mock { /// This class is internally locked to allow tests to modify the in-flight requests while /// the API is also holding a pointer to the same data structure. pub struct MockEntropyReader { + block_number: RwLock, /// The set of requests that are currently in-flight. - requests: RwLock>, + requests: RwLock>, } impl MockEntropyReader { - pub fn with_requests(requests: &[(Address, u64)]) -> MockEntropyReader { + pub fn with_requests( + block_number: BlockNumber, + requests: &[(Address, u64, BlockNumber, bool)], + ) -> MockEntropyReader { MockEntropyReader { - requests: RwLock::new( + block_number: RwLock::new(block_number), + requests: RwLock::new( requests .iter() - .map(|&(a, s)| Request { + .map(|&(a, s, b, u)| Request { provider: a, sequence_number: s, + block_number: b, + use_blockhash: u, }) .collect(), ), @@ -61,13 +76,26 @@ pub mod mock { } /// Insert a new request into the set of in-flight requests. - pub fn insert(&self, provider: Address, sequence: u64) -> &Self { + pub fn insert( + &self, + provider: Address, + sequence: u64, + block_number: BlockNumber, + use_blockhash: bool, + ) -> &Self { self.requests.write().unwrap().push(Request { provider, sequence_number: sequence, + block_number, + use_blockhash, }); self } + + pub fn set_block_number(&self, block_number: BlockNumber) -> &Self { + *(self.block_number.write().unwrap()) = block_number; + self + } } #[async_trait] @@ -85,5 +113,9 @@ pub mod mock { .find(|&r| r.sequence_number == sequence_number && r.provider == provider) .map(|r| (*r).clone())) } + + async fn get_block_number(&self) -> Result { + Ok(*self.block_number.read().unwrap()) + } } } diff --git a/fortuna/src/command/run.rs b/fortuna/src/command/run.rs index 333d6e8233..7e0bba4af8 100644 --- a/fortuna/src/command/run.rs +++ b/fortuna/src/command/run.rs @@ -88,6 +88,7 @@ pub async fn run(opts: &RunOptions) -> Result<()> { state: Arc::new(chain_state), contract, provider_address: opts.provider, + reveal_delay_blocks: chain_config.reveal_delay_blocks, }; chains.insert(chain_id.clone(), state); diff --git a/fortuna/src/config.rs b/fortuna/src/config.rs index 71e80f9004..dd3942c7ea 100644 --- a/fortuna/src/config.rs +++ b/fortuna/src/config.rs @@ -1,5 +1,8 @@ use { - crate::api::ChainId, + crate::{ + api::ChainId, + chain::reader::BlockNumber, + }, anyhow::{ anyhow, Result, @@ -115,6 +118,9 @@ pub struct EthereumConfig { /// Address of a Pyth Randomness contract to interact with. pub contract_addr: Address, + /// How many blocks to wait before revealing the random number. + pub reveal_delay_blocks: BlockNumber, + /// Use the legacy transaction format (for networks without EIP 1559) #[serde(default)] pub legacy_tx: bool, diff --git a/fortuna/src/config/register_provider.rs b/fortuna/src/config/register_provider.rs index b971dc2cae..a97fd5236a 100644 --- a/fortuna/src/config/register_provider.rs +++ b/fortuna/src/config/register_provider.rs @@ -7,7 +7,6 @@ use { }, }, clap::Args, - ethers::types::U256, }; #[derive(Args, Clone, Debug)] @@ -34,7 +33,7 @@ pub struct RegisterProviderOptions { /// The fee to charge (in wei) for each requested random number #[arg(long = "pyth-contract-fee")] #[arg(default_value = "100")] - pub fee: U256, + pub fee: u128, /// The URI where clients can retrieve random values from this provider, /// i.e., wherever fortuna for this provider will be hosted. diff --git a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol index 58f542067d..a4a456bd13 100644 --- a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol +++ b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol @@ -209,11 +209,8 @@ abstract contract Entropy is IEntropy, EntropyState { ); req.requester = msg.sender; - if (useBlockHash) { - req.blockNumber = SafeCast.toUint96(block.number); - } else { - req.blockNumber = 0; - } + req.blockNumber = SafeCast.toUint64(block.number); + req.useBlockhash = useBlockHash; emit Requested(req); } @@ -255,7 +252,7 @@ abstract contract Entropy is IEntropy, EntropyState { ) revert EntropyErrors.IncorrectRevelation(); bytes32 blockHash = bytes32(uint256(0)); - if (req.blockNumber != 0) { + if (req.useBlockhash) { blockHash = blockhash(req.blockNumber); } diff --git a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol index 57a44b0d41..7d26dc18ec 100644 --- a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol @@ -205,8 +205,13 @@ contract EntropyTest is Test, EntropyTestUtils { } function testBasicFlow() public { + vm.roll(17); uint64 sequenceNumber = request(user2, provider1, 42, false); - assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 0); + assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 17); + assertEq( + random.getRequest(provider1, sequenceNumber).useBlockhash, + false + ); assertRevealSucceeds( user2, @@ -229,13 +234,18 @@ contract EntropyTest is Test, EntropyTestUtils { } function testDefaultProvider() public { + vm.roll(20); uint64 sequenceNumber = request( user2, random.getDefaultProvider(), 42, false ); - assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 0); + assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 20); + assertEq( + random.getRequest(provider1, sequenceNumber).useBlockhash, + false + ); assertRevealReverts( user2, @@ -390,6 +400,10 @@ contract EntropyTest is Test, EntropyTestUtils { random.getRequest(provider1, sequenceNumber).blockNumber, 1234 ); + assertEq( + random.getRequest(provider1, sequenceNumber).useBlockhash, + true + ); assertRevealSucceeds( user2, diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol index b561a7f7be..8117c3d7c5 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol @@ -45,11 +45,15 @@ contract EntropyStructs { // eliminating 1 store. bytes32 commitment; // Storage slot 3 // - // If nonzero, the randomness requester wants the blockhash of this block to be incorporated into the random number. - // Note that we're using a uint96 such that we have an additional 20 bytes of storage afterward for an address. - // Although block.number returns a uint256, 96 bits should be plenty to index all of the blocks ever generated. - uint96 blockNumber; + // The number of the block where this request was created. + // Note that we're using a uint64 such that we have an additional space for an address and other fields in + // this storage slot. Although block.number returns a uint256, 64 bits should be plenty to index all of the + // blocks ever generated. + uint64 blockNumber; // The address that requested this random number. address requester; + // If true, incorporate the blockhash of blockNumber into the generated random value. + bool useBlockhash; + // There are 3 remaining bytes of free space in this slot. } }