Skip to content

Commit

Permalink
solana-ibc: introduce basic interface for handling the guest chain (#85)
Browse files Browse the repository at this point in the history
Add contract calls for managing the guest blockchain. This includes
initialising the chain, generating new blocks, adding signatures and
candidates management.

The implementation is purposefully bare bones with no handling for
rewards or slashing so that the code can be tested and those features
added later on.
  • Loading branch information
mina86 authored Nov 13, 2023
1 parent e8aaa88 commit 5063b56
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 45 deletions.
8 changes: 6 additions & 2 deletions common/blockchain/src/candidates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ mod tests;
///
/// Whenever epoch changes, candidates with most stake are included in
/// validators set.
#[derive(Clone, PartialEq, Eq)]
#[derive(
Clone, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize,
)]
pub struct Candidates<PK> {
/// Maximum number of validators in a validator set.
max_validators: NonZeroU16,
Expand All @@ -29,7 +31,9 @@ pub struct Candidates<PK> {
}

/// A candidate to become a validator.
#[derive(Clone, PartialEq, Eq)]
#[derive(
Clone, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize,
)]
struct Candidate<PK> {
/// Public key of the candidate.
pubkey: PK,
Expand Down
2 changes: 1 addition & 1 deletion common/blockchain/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use core::num::{NonZeroU128, NonZeroU16};
///
/// Those are not encoded within a blockchain and only matter when generating
/// a new block.
// TODO(mina86): Do those configuration options make sense?
#[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)]
pub struct Config {
/// Minimum number of validators allowed in an epoch.
///
Expand Down
2 changes: 2 additions & 0 deletions common/blockchain/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use lib::hash::CryptoHash;

pub use crate::candidates::UpdateCandidateError;

#[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)]
pub struct ChainManager<PK> {
/// Configuration specifying limits for block generation.
config: crate::Config,
Expand Down Expand Up @@ -35,6 +36,7 @@ pub struct ChainManager<PK> {
/// Pending block waiting for signatures.
///
/// Once quorum of validators sign the block it’s promoted to the current block.
#[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)]
struct PendingBlock<PK> {
/// The block that waits for signatures.
next_block: crate::Block<PK>,
Expand Down
203 changes: 203 additions & 0 deletions solana/solana-ibc/programs/solana-ibc/src/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program;
pub use blockchain::Config;

use crate::error::Error;
use crate::{events, storage};

type Result<T = (), E = anchor_lang::error::Error> = core::result::Result<T, E>;

pub type Epoch = blockchain::Epoch<PubKey>;
pub type Block = blockchain::Block<PubKey>;
pub type Manager = blockchain::ChainManager<PubKey>;
pub use crate::ed25519::{PubKey, Signature, Verifier};

/// Guest blockchain data held in Solana account.
#[account]
pub struct ChainData {
inner: Option<ChainInner>,
}

impl ChainData {
/// Initialises a new guest blockchain with given configuration and genesis
/// epoch.
///
/// Fails if the chain is already initialised.
pub fn initialise(
&mut self,
trie: &mut storage::AccountTrie,
config: Config,
genesis_epoch: Epoch,
) -> Result {
let (height, timestamp) = host_head()?;
let genesis = Block::generate_genesis(
1.into(),
height,
timestamp,
trie.hash().clone(),
genesis_epoch,
)
.map_err(|err| Error::Internal(err.into()))?;
let manager =
Manager::new(config, genesis.clone()).map_err(Error::from)?;
if self.inner.is_some() {
return Err(Error::ChainAlreadyInitialised.into());
}
let last_check_height = manager.head().1.host_height;
let inner =
self.inner.insert(ChainInner { last_check_height, manager });

let (finalised, head) = inner.manager.head();
assert!(finalised);
events::emit(events::Initialised {
genesis: events::NewBlock { hash: &head.calc_hash(), block: head },
})
.map_err(ProgramError::BorshIoError)?;
Ok(())
}

/// Generates a new guest block.
///
/// Fails if a new block couldn’t be created. This can happen if head of the
/// guest blockchain is pending (not signed by quorum of validators) or criteria
/// for creating a new block haven’t been met (e.g. state hasn’t changed).
///
/// This is intended as handling an explicit contract call for generating a new
/// block. In contrast, [`maybe_generate_block`] is intended to create a new
/// block opportunistically at the beginning of handling any smart contract
/// request.
pub fn generate_block(&mut self, trie: &storage::AccountTrie) -> Result {
self.generate_block_impl(trie, true)
}

/// Generates a new guest block if possible.
///
/// Contrary to [`generate_block`] this function won’t fail if new block could
/// not be created.
///
/// This is intended to create a new block opportunistically at the beginning of
/// handling any smart contract request.
pub fn maybe_generate_block(
&mut self,
trie: &storage::AccountTrie,
) -> Result {
self.generate_block_impl(trie, false)
}

/// Attempts generating a new guest block.
///
/// Implementation of [`generate_block`] and [`maybe_generate_block`] functions.
/// If `force` is `true` and new block is not generated, returns an error.
/// Otherwise, failure to generate a new block (e.g. because there’s one pending
/// or state hasn’t changed) is silently ignored.
fn generate_block_impl(
&mut self,
trie: &storage::AccountTrie,
force: bool,
) -> Result {
let inner = self.get_mut()?;
let (height, timestamp) = host_head()?;

// We attempt generating guest blocks only once per host block. This has
// two reasons:
// 1. We don’t want to repeat the same checks each block.
// 2. We don’t want a situation where some IBC packets are created during
// a Solana block but only some of them end up in a guest block generated
// during that block.
if inner.last_check_height == height {
return if force {
Err(Error::GenerationAlreadyAttempted.into())
} else {
Ok(())
};
}
inner.last_check_height = height;
let res = inner.manager.generate_next(
height,
timestamp,
trie.hash().clone(),
false,
);
match res {
Ok(()) => {
let (finalised, head) = inner.manager.head();
assert!(!finalised);
events::emit(events::NewBlock {
hash: &head.calc_hash(),
block: head,
})
.map_err(ProgramError::BorshIoError)?;
Ok(())
}
Err(err) if force => Err(into_error(err)),
Err(_) => Ok(()),
}
}

/// Submits a signature for the pending block.
///
/// If quorum of signatures has been reached returns `true`. Otherwise
/// returns `false`. This operation is idempotent. Submitting the same
/// signature multiple times has no effect (other than wasting gas).
pub fn sign_block(
&mut self,
pubkey: PubKey,
signature: &Signature,
verifier: &Verifier,
) -> Result<bool> {
let manager = &mut self.get_mut()?.manager;
let res = manager
.add_signature(pubkey.clone(), signature, verifier)
.map_err(into_error)?;

let mut hash = None;
if res.got_new_signature() {
let hash = hash.get_or_insert_with(|| manager.head().1.calc_hash());
events::emit(events::BlockSigned {
block_hash: hash,
pubkey: &pubkey,
})
.map_err(ProgramError::BorshIoError)?;
}
if res.got_quorum() {
let hash = hash.get_or_insert_with(|| manager.head().1.calc_hash());
events::emit(events::BlockFinalised { block_hash: hash })
.map_err(ProgramError::BorshIoError)?;
}
Ok(res.got_quorum())
}

/// Updates validator’s stake.
pub fn set_stake(&mut self, pubkey: PubKey, amount: u128) -> Result<()> {
self.get_mut()?
.manager
.update_candidate(pubkey, amount)
.map_err(into_error)
}

/// Returns mutable the inner chain data if it has been initialised.
fn get_mut(&mut self) -> Result<&mut ChainInner> {
self.inner.as_mut().ok_or_else(|| Error::ChainNotInitialised.into())
}
}

fn into_error<E: Into<Error>>(err: E) -> anchor_lang::error::Error {
err.into().into()
}

/// The inner chain data
#[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)]
struct ChainInner {
/// Last Solana block at which last check for new guest block generation was
/// performed.
last_check_height: blockchain::HostHeight,

/// The guest blockchain manager handling generation of new guest blocks.
manager: Manager,
}

/// Returns Solana block height and timestamp.
fn host_head() -> Result<(blockchain::HostHeight, u64)> {
let clock = solana_program::clock::Clock::get()?;
Ok((clock.slot.into(), clock.unix_timestamp.try_into().unwrap()))
}
24 changes: 19 additions & 5 deletions solana/solana-ibc/programs/solana-ibc/src/ed25519.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anchor_lang::prelude::{borsh, err};
use anchor_lang::solana_program::account_info::AccountInfo;
use anchor_lang::solana_program::{ed25519_program, sysvar};
use anchor_lang::solana_program;
use solana_program::account_info::AccountInfo;
use solana_program::{ed25519_program, sysvar};

/// An Ed25519 public key used by guest validators to sign guest blocks.
#[derive(
Expand All @@ -12,13 +13,25 @@ use anchor_lang::solana_program::{ed25519_program, sysvar};
Hash,
borsh::BorshSerialize,
borsh::BorshDeserialize,
derive_more::From,
derive_more::Into,
)]
pub struct PubKey([u8; Self::LENGTH]);
pub struct PubKey([u8; 32]);

impl PubKey {
pub const LENGTH: usize = 32;
}

impl From<solana_program::pubkey::Pubkey> for PubKey {
fn from(pubkey: solana_program::pubkey::Pubkey) -> Self {
Self(pubkey.to_bytes())
}
}

impl From<PubKey> for solana_program::pubkey::Pubkey {
fn from(pubkey: PubKey) -> Self { Self::from(pubkey.0) }
}

impl blockchain::PubKey for PubKey {
type Signature = Signature;
}
Expand All @@ -33,8 +46,10 @@ impl blockchain::PubKey for PubKey {
Hash,
borsh::BorshSerialize,
borsh::BorshDeserialize,
derive_more::From,
derive_more::Into,
)]
pub struct Signature([u8; Self::LENGTH]);
pub struct Signature([u8; 64]);

impl Signature {
pub const LENGTH: usize = 64;
Expand All @@ -61,7 +76,6 @@ impl Verifier {
/// Returns error if `ix_sysver` is not `AccountInfo` for the Instruction
/// sysvar, there was no instruction prior to the current on or the previous
/// instruction was not a call to the Ed25519 native program.
#[allow(dead_code)]
pub fn new(ix_sysvar: &AccountInfo<'_>) -> anchor_lang::Result<Self> {
let ix = sysvar::instructions::get_instruction_relative(-1, ix_sysvar)?;
if ed25519_program::check_id(&ix.program_id) {
Expand Down
Loading

0 comments on commit 5063b56

Please sign in to comment.