logoDeveloper Hub
TimeStamp VM

Blocks

In this section, we will examine the Block data structure. In contrast to the design choice of the TimestampVM state (which was mostly in control of the implementers), blocks in TimestampVM must adhere to the SnowmanVM Block Interface.

SnowmanVM.Block Interface

TimestampVM is designed to be used in tandem with the Snowman Consensus Engine. In particular, this relationship is defined by the usage of blocks - TimestampVM will produce blocks which the Snowman Consensus Engine will use and eventually mark as accepted or rejected. Therefore, the Snowman Consensus Engine requires for all blocks to have the following interface:

type Block interface {
	choices.Decidable
 
	// Parent returns the ID of this block's parent.
	Parent() ids.ID
 
	// Verify that the state transition this block would make if accepted is
	// valid. If the state transition is invalid, a non-nil error should be
	// returned.
	//
	// It is guaranteed that the Parent has been successfully verified.
	//
	// If nil is returned, it is guaranteed that either Accept or Reject will be
	// called on this block, unless the VM is shut down.
	Verify(context.Context) error
 
	// Bytes returns the binary representation of this block.
	//
	// This is used for sending blocks to peers. The bytes should be able to be
	// parsed into the same block on another node.
	Bytes() []byte
 
	// Height returns the height of this block in the chain.
	Height() uint64
 
	// Time this block was proposed at. This value should be consistent across
	// all nodes. If this block hasn't been successfully verified, any value can
	// be returned. If this block is the last accepted block, the timestamp must
	// be returned correctly. Otherwise, accepted blocks can return any value.
	Timestamp() time.Time
}

Implementing the Block Data Structure

With the above in mind, we now examine the block data structure:

/// Represents a block, specific to `Vm` (crate::vm::Vm).
#[serde_as]
#[derive(Serialize, Deserialize, Clone, Derivative, Default)]
#[derivative(Debug, PartialEq, Eq)]
pub struct Block {
    /// The block Id of the parent block.
    parent_id: ids::Id,
    /// This block's height.
    /// The height of the genesis block is 0.
    height: u64,
    /// Unix second when this block was proposed.
    timestamp: u64,
    /// Arbitrary data.
    #[serde_as(as = "Hex0xBytes")]
    data: Vec<u8>,
 
    /// Current block status.
    #[serde(skip)]
    status: choices::status::Status,
    /// This block's encoded bytes.
    #[serde(skip)]
    bytes: Vec<u8>,
    /// Generated block Id.
    #[serde(skip)]
    id: ids::Id,
 
    /// Reference to the Vm state manager for blocks.
    #[derivative(Debug = "ignore", PartialEq = "ignore")]
    #[serde(skip)]
    state: state::State,
}

Notice above that many of the fields of the Block struct store the information required to implement the Snowman.Block interface we saw previously. Tying the concept of Blocks back to the VM State, notice the last field state within the Block struct. This is where the Block struct stores a copy of the State struct from the previous section (and since each field of the State struct is wrapped in an Arc pointer, this implies that Block is really just storing a reference to both the db and verified_blocks data structures).

Block Functions

In this section, we examine some of the functions associated with the Block struct:

verify

This function verifies that a block is valid and stores it in memory. Note that a verified block does not mean that it has been accepted - rather, a verified block is eligible to be accepted.

/// Verifies [`Block`](Block) properties (e.g., heights),
/// and once verified, records it to the `State` (crate::state::State).
/// # Errors
/// Can fail if the parent block can't be retrieved.
pub async fn verify(&mut self) -> io::Result<()> {
    if self.height == 0 && self.parent_id == ids::Id::empty() {
        log::debug!(
            "block {} has an empty parent Id since it's a genesis block -- skipping verify",
            self.id
        );
        self.state.add_verified(&self.clone()).await;
        return Ok(());
    }
 
    // if already exists in database, it means it's already accepted
    // thus no need to verify once more
    if self.state.get_block(&self.id).await.is_ok() {
        log::debug!("block {} already verified", self.id);
        return Ok(());
    }
 
    let prnt_blk = self.state.get_block(&self.parent_id).await?;
 
    // ensure the height of the block is immediately following its parent
    if prnt_blk.height != self.height - 1 {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "parent block height {} != current block height {} - 1",
                prnt_blk.height, self.height
            ),
        ));
    }
 
    // ensure block timestamp is after its parent
    if prnt_blk.timestamp > self.timestamp {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "parent block timestamp {} > current block timestamp {}",
                prnt_blk.timestamp, self.timestamp
            ),
        ));
    }
 
    let one_hour_from_now = Utc::now() + Duration::hours(1);
    let one_hour_from_now = one_hour_from_now
        .timestamp()
        .try_into()
        .expect("failed to convert timestamp from i64 to u64");
 
    // ensure block timestamp is no more than an hour ahead of this nodes time
    if self.timestamp >= one_hour_from_now {
        return Err(Error::new(
            ErrorKind::InvalidData,
            format!(
                "block timestamp {} is more than 1 hour ahead of local time",
                self.timestamp
            ),
        ));
    }
 
    // add newly verified block to memory
    self.state.add_verified(&self.clone()).await;
    Ok(())
}

reject

When called by the Snowman Consensus Engine, this tells the VM that the particular block has been rejected.

/// Mark this [`Block`](Block) rejected and updates `State` (crate::state::State) accordingly.
/// # Errors
/// Returns an error if the state can't be updated.
pub async fn reject(&mut self) -> io::Result<()> {
    self.set_status(choices::status::Status::Rejected);
 
    // only decided blocks are persistent -- no reorg
    self.state.write_block(&self.clone()).await?;
 
    self.state.remove_verified(&self.id()).await;
    Ok(())
}

accept

When called by the Snowman Consensus Engine, this tells the VM that the particular block has been rejected.

/// Mark this [`Block`](Block) accepted and updates `State` (crate::state::State) accordingly.
/// # Errors
/// Returns an error if the state can't be updated.
pub async fn accept(&mut self) -> io::Result<()> {
    self.set_status(choices::status::Status::Accepted);
 
    // only decided blocks are persistent -- no reorg
    self.state.write_block(&self.clone()).await?;
    self.state.set_last_accepted_block(&self.id()).await?;
 
    self.state.remove_verified(&self.id()).await;
    Ok(())
}

Last updated on

On this page

Edit on Github