Skip to main content

State in TimestampVM

Blockchains can be defined as follows:

A linked-list where each list element consists of a block

Implementations of blockchains, while adhering to the functionality above from a white-box perspective, are defined much more like databases than regular linked lists themselves. In fact, this is what TimestampVM utilizes! By utilizing a general database, TimestampVM is able to store blocks (and thus, store its blockchain) while also being able to store additional data such as pending blocks.

State Definition

Below is the definition of the State struct which is used to maintain the state of the TimestampVM:

/// Manages block and chain states for this Vm, both in-memory and persistent.
#[derive(Clone)]
pub struct State {
pub db: Arc<RwLock<Box<dyn subnet::rpc::database::Database + Send + Sync>>>,

/// Maps block Id to Block.
/// Each element is verified but not yet accepted/rejected (e.g., preferred).
pub verified_blocks: Arc<RwLock<HashMap<ids::Id, Block>>>,
}

State in this context acts like the database of TimestampVM. Within State, we are managing two different data structures:

  • db: a byte-based mapping which maps bytes to bytes. This is where finalized (that is, accepted blocks are stored)
  • verified_blocks: a hashmap which maps block numbers to their respective blocks. This is where all verified, but pending blocks are stored

While one could have guessed the functionalities of db and verified_blocks from their respective types subnet::rpc::database::Database + Send + Sync and HashMap<ids::Id, Block>, it is not immediately clear why we are wrapping these fields with Read/Write locks and Arc pointers. However, as we'll see soon when we examine the Block data structure, blocks need access to the VM state so they can add themselves to state. This is due to the SetPrefernce function of SnowmanVM interface, which states:

Set Preference

The VM implements the function SetPreference(blkID ids.ID) to allow the consensus engine to notify the VM which block is currently preferred to be accepted. The VM should use this information to set the head of its blockchain. Most importantly, when the consensus engine calls BuildBlock, the VM should be sure to build on top of the block that is the most recently set preference.

Note: SetPreference will always be called with a block that has no verified children.

Therefore, when building a Rust-based VM (or a VM in any supported language), the VM itself is only responsible for tracking the ID of the most recent finalized block; blocks bear the responsibility of storing themselves in VM state. As a result, we will need to wrap the db and verified_blocks fields with the following:

  • An Arc pointer so that whenever we clone the State structure, the cloned versions of db and verified_blocks are still pointing to the same data structures in memory. This allows for multiple Blocks to share the same db and verified_blocks
  • A read/write lock (that is, RwLock) so that we safely utilize concurrency in our VM

State Functions

Below are the functions associated with the State struct:

timestampvm/src/state/mod.rs
impl State {
/// Persists the last accepted block Id to state.
/// # Errors
/// Fails if the db can't be updated
pub async fn set_last_accepted_block(&self, blk_id: &ids::Id) -> io::Result<()> {
let mut db = self.db.write().await;
db.put(LAST_ACCEPTED_BLOCK_KEY, &blk_id.to_vec())
.await
.map_err(|e| {
Error::new(
ErrorKind::Other,
format!("failed to put last accepted block: {e:?}"),
)
})
}

/// Returns "true" if there's a last accepted block found.
/// # Errors
/// Fails if the db can't be read
pub async fn has_last_accepted_block(&self) -> io::Result<bool> {
let db = self.db.read().await;
match db.has(LAST_ACCEPTED_BLOCK_KEY).await {
Ok(found) => Ok(found),
Err(e) => Err(Error::new(
ErrorKind::Other,
format!("failed to load last accepted block: {e}"),
)),
}
}

/// Returns the last accepted block Id from state.
/// # Errors
/// Can fail if the db can't be read
pub async fn get_last_accepted_block_id(&self) -> io::Result<ids::Id> {
let db = self.db.read().await;
match db.get(LAST_ACCEPTED_BLOCK_KEY).await {
Ok(d) => Ok(ids::Id::from_slice(&d)),
Err(e) => {
if subnet::rpc::errors::is_not_found(&e) {
return Ok(ids::Id::empty());
}
Err(e)
}
}
}

/// Adds a block to "`verified_blocks`".
pub async fn add_verified(&mut self, block: &Block) {
let blk_id = block.id();
log::info!("verified added {blk_id}");

let mut verified_blocks = self.verified_blocks.write().await;
verified_blocks.insert(blk_id, block.clone());
}

/// Removes a block from "`verified_blocks`".
pub async fn remove_verified(&mut self, blk_id: &ids::Id) {
let mut verified_blocks = self.verified_blocks.write().await;
verified_blocks.remove(blk_id);
}

/// Returns "true" if the block Id has been already verified.
pub async fn has_verified(&self, blk_id: &ids::Id) -> bool {
let verified_blocks = self.verified_blocks.read().await;
verified_blocks.contains_key(blk_id)
}

/// Writes a block to the state storage.
/// # Errors
/// Can fail if the block fails to serialize or if the db can't be updated
pub async fn write_block(&mut self, block: &Block) -> io::Result<()> {
let blk_id = block.id();
let blk_bytes = block.to_vec()?;

let mut db = self.db.write().await;

let blk_status = BlockWithStatus {
block_bytes: blk_bytes,
status: block.status(),
};
let blk_status_bytes = blk_status.encode()?;

db.put(&block_with_status_key(&blk_id), &blk_status_bytes)
.await
.map_err(|e| Error::new(ErrorKind::Other, format!("failed to put block: {e:?}")))
}

/// Reads a block from the state storage using the `block_with_status_key`.
/// # Errors
/// Can fail if the block is not found in the state storage, or if the block fails to deserialize
pub async fn get_block(&self, blk_id: &ids::Id) -> io::Result<Block> {
// check if the block exists in memory as previously verified.
let verified_blocks = self.verified_blocks.read().await;
if let Some(b) = verified_blocks.get(blk_id) {
return Ok(b.clone());
}

let db = self.db.read().await;

let blk_status_bytes = db.get(&block_with_status_key(blk_id)).await?;
let blk_status = BlockWithStatus::from_slice(blk_status_bytes)?;

let mut blk = Block::from_slice(&blk_status.block_bytes)?;
blk.set_status(blk_status.status);

Ok(blk)
}
}

The functions above are called will be called by both blocks and the VM itself.

Was this page helpful?