Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"bridge",
"common",
"chain",
"clob",
"coins",
"crypto",
"dkg",
Expand Down Expand Up @@ -39,6 +40,7 @@ nunchi-coins = { version = "2026.5.0", path = "coins" }
nunchi-authority = { version = "2026.5.0", path = "authority" }
nunchi-bridge = { version = "2026.5.0", path = "bridge" }
nunchi-chain = { version = "2026.5.0", path = "chain" }
nunchi-clob = { version = "2026.5.0", path = "clob" }
nunchi-common = { version = "2026.5.0", path = "common" }
nunchi-crypto = { version = "2026.5.0", path = "crypto" }
nunchi-dkg = { version = "2026.5.0", path = "dkg" }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ This repository will contain modules for building public and private blockchains
* `nunchi-margin` - user has BTC + nunchi and doesn't want to sell, and deposits BTC+nunchi and gets a stablecoin. Could be backed by other coins, not just btc and nunchi.
* `nunchi-securities` - Non-synthetic perps contracts (delivery of tokenized stock)
* `nunchi-vaults` - a module for running vaults composed of many types of capital, traded by an authorised offchain party
* `nunchi-clob` - used on the global chain, provides liquidity between local chain tokens
* [`nunchi-clob`](clob/) - used on the global chain, provides liquidity between local chain tokens
* `nunchi-derivatives` - ingests a price feed and creates derivatives products
* `nunchi-stablecoin` - a wrapper of coins special for the needs of stablecoins

Expand Down
130 changes: 108 additions & 22 deletions chain/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,78 @@ where
Some((included, merkleized))
}

async fn build_proposal_state<E: Storage + Clock + Metrics>(
&mut self,
timing: Option<(&E, &ApplicationMetrics)>,
batches: <QmdbDatabaseSet<E> as DatabaseSet<E>>::Unmerkleized,
context: RuntimeContext,
candidates: Vec<R::Transaction>,
) -> Option<(Vec<R::Transaction>, Ext::Payload, QmdbMerkleized<E>)> {
let mut batch = QmdbBatch::new(batches);
let mut included = Vec::new();

let validate_start = timing.map(|(runtime_context, _)| runtime_context.current());
for transaction in candidates {
if included.len() == self.max_block_transactions {
break;
}
let mut overlay = Overlay::new(&mut batch);
match R::validate(&mut overlay, context, &transaction).await {
Ok(()) => {
overlay.commit();
included.push(transaction);
}
Err(error) if R::is_storage_error(&error) => {
error!(?error, "storage failure while building block");
return None;
}
Err(error) => {
debug!(?error, "skipping non-executable txpool transaction");
}
}
}
if let Some(((runtime_context, metrics), validate_start)) = timing.zip(validate_start) {
metrics
.proposal_validate_duration
.observe_between(validate_start, runtime_context.current());
}

let extension = self.consensus.propose().await;
if !self
.consensus
.apply_payload(&mut batch, context, &extension)
.await
{
return None;
}

let merkleize_start = timing.map(|(runtime_context, _)| runtime_context.current());
let merkleized = match batch.merkleize().await {
Ok(merkleized) => merkleized,
Err(error) => {
error!(?error, "merkleization failed while building block");
return None;
}
};
if let Some(((runtime_context, metrics), merkleize_start)) = timing.zip(merkleize_start) {
metrics
.proposal_merkleize_duration
.observe_between(merkleize_start, runtime_context.current());
}
Some((included, extension, merkleized))
}

#[allow(clippy::too_many_arguments)]
async fn execute_block<E, EventHandler>(
&mut self,
runtime_context: &E,
metrics: &ApplicationMetrics,
batches: <QmdbDatabaseSet<E> as DatabaseSet<E>>::Unmerkleized,
context: RuntimeContext,
transactions: &[R::Transaction],
extension: &Ext::Payload,
events: &EventHandler,
commit_extension: bool,
) -> Option<QmdbMerkleized<E>>
where
E: Storage + Clock + Metrics,
Expand Down Expand Up @@ -309,6 +374,21 @@ where
}
}
}
if !self
.consensus
.apply_payload(&mut batch, context, extension)
.await
{
if let Some(digest) = context.block_digest {
events.discard_block(digest).await;
}
return None;
}
if commit_extension {
self.consensus
.commit_payload(&mut batch, context, extension)
.await;
}
metrics
.apply_transactions_duration
.observe_between(apply_start, runtime_context.current());
Expand Down Expand Up @@ -551,8 +631,8 @@ where
parent.height.next(),
timestamp,
);
let (transactions, merkleized) = self
.build_valid_transactions_inner(
let (transactions, extension, merkleized) = self
.build_proposal_state(
Some((&runtime_context, &metrics)),
batches,
execution_context,
Expand All @@ -564,7 +644,6 @@ where
Some(dkg) => dkg.act().await,
None => None,
};
let extension = self.consensus.propose().await;
let block = Block::new(
context,
parent.digest(),
Expand Down Expand Up @@ -619,15 +698,18 @@ where
}

let execution_context = Self::block_runtime_context(&block);
let merkleized = Self::execute_block(
&runtime_context,
&metrics,
batches,
execution_context,
&block.transactions,
&NoopEventConsumer,
)
.await?;
let merkleized = self
.execute_block(
&runtime_context,
&metrics,
batches,
execution_context,
&block.transactions,
&block.extension,
&NoopEventConsumer,
false,
)
.await?;
let state_range = Self::state_range(&merkleized);
if merkleized.root() != block.state_root || state_range != block.state_range {
return None;
Expand All @@ -643,16 +725,20 @@ where
) -> <Self::Databases as DatabaseSet<E>>::Merkleized {
let metrics = self.metrics(&runtime_context);
let execution_context = Self::block_runtime_context(block);
let merkleized = Self::execute_block(
&runtime_context,
&metrics,
batches,
execution_context,
&block.transactions,
&self.events,
)
.await
.expect("certified block failed deterministic execution");
let events = self.events.clone();
let merkleized = self
.execute_block(
&runtime_context,
&metrics,
batches,
execution_context,
&block.transactions,
&block.extension,
&events,
true,
)
.await
.expect("certified block failed deterministic execution");
let state_range = Self::state_range(&merkleized);
assert_eq!(
merkleized.root(),
Expand Down
52 changes: 52 additions & 0 deletions chain/src/consensus/extension.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{fmt::Debug, future::Future};

use commonware_codec::{EncodeSize, Read, Write};
use nunchi_common::{RuntimeContext, StateStore};

/// Consensus-side payload carried by blocks but driven outside ordinary runtime transactions.
pub trait BlockExtension: 'static {
Expand Down Expand Up @@ -34,6 +35,32 @@ pub trait ConsensusExtension: BlockExtension + Clone + Send + 'static {
fn verify_payload(&mut self, _payload: &Self::Payload) -> impl Future<Output = bool> + Send {
std::future::ready(true)
}

/// Apply the extension payload to the same authenticated state as runtime transactions.
fn apply_payload<S>(
&mut self,
_state: &mut S,
_context: RuntimeContext,
_payload: &Self::Payload,
) -> impl Future<Output = bool> + Send
where
S: StateStore + Send + Sync,
{
std::future::ready(true)
}

/// Run extension side effects that should happen only for a certified block.
fn commit_payload<S>(
&mut self,
_state: &mut S,
_context: RuntimeContext,
_payload: &Self::Payload,
) -> impl Future<Output = ()> + Send
where
S: StateStore + Send + Sync,
{
std::future::ready(())
}
}

/// Pair of extra consensus extensions carried in one block extension slot.
Expand Down Expand Up @@ -73,6 +100,31 @@ where
async fn verify_payload(&mut self, payload: &Self::Payload) -> bool {
self.0.verify_payload(&payload.0).await && self.1.verify_payload(&payload.1).await
}

async fn apply_payload<S>(
&mut self,
state: &mut S,
context: RuntimeContext,
payload: &Self::Payload,
) -> bool
where
S: StateStore + Send + Sync,
{
self.0.apply_payload(state, context, &payload.0).await
&& self.1.apply_payload(state, context, &payload.1).await
}

async fn commit_payload<S>(
&mut self,
state: &mut S,
context: RuntimeContext,
payload: &Self::Payload,
) where
S: StateStore + Send + Sync,
{
self.0.commit_payload(state, context, &payload.0).await;
self.1.commit_payload(state, context, &payload.1).await;
}
}

/// Empty extra consensus extension for chains without additional non-DKG payloads.
Expand Down
59 changes: 58 additions & 1 deletion chain/tests/block_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use commonware_consensus::types::{Epoch, Height, Round, View};
use commonware_cryptography::{ed25519, sha256, Digest as _, Digestible as _, Signer};
use commonware_storage::mmr::Location;
use commonware_utils::{non_empty_range, NZU32};
use nunchi_chain::{Block, BlockExtension, Composite, ConsensusExtension, StateCommitment};
use nunchi_chain::{
Block, BlockExtension, Composite, ConsensusExtension, NoConsensusExtension, StateCommitment,
};
use nunchi_common::{RuntimeContext, StateError, StateStore};
use nunchi_dkg::{Context, ReshareBlock};

#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -65,6 +68,31 @@ impl ConsensusExtension for TestConsensusExtension {
) -> impl std::future::Future<Output = bool> + Send {
std::future::ready(payload.0 == self.0)
}

async fn apply_payload<S>(
&mut self,
_: &mut S,
_: RuntimeContext,
payload: &Self::Payload,
) -> bool
where
S: StateStore + Send + Sync,
{
payload.0 == self.0
}
}

#[derive(Default)]
struct NoopState;

impl StateStore for NoopState {
async fn get(&self, _: &sha256::Digest) -> Result<Option<Vec<u8>>, StateError> {
Ok(None)
}

fn set(&mut self, _: sha256::Digest, _: Vec<u8>) {}

fn remove(&mut self, _: sha256::Digest) {}
}

fn context() -> Context {
Expand Down Expand Up @@ -205,6 +233,35 @@ fn composite_consensus_extension_verifies_both_payloads() {
));
}

#[test]
fn default_consensus_extension_applies_noop_payload() {
let mut extension = NoConsensusExtension;
let mut state = NoopState;

assert!(futures::executor::block_on(extension.apply_payload(
&mut state,
RuntimeContext::default(),
&()
)));
}

#[test]
fn composite_consensus_extension_applies_both_payloads() {
let mut extension = Composite::new(TestConsensusExtension(1), TestConsensusExtension(2));
let mut state = NoopState;

assert!(futures::executor::block_on(extension.apply_payload(
&mut state,
RuntimeContext::default(),
&(TestPayload(1), TestPayload(2))
)));
assert!(!futures::executor::block_on(extension.apply_payload(
&mut state,
RuntimeContext::default(),
&(TestPayload(1), TestPayload(3))
)));
}

#[test]
fn dkg_reshare_log_is_core_block_field() {
let block = Block::<u8>::new(
Expand Down
Loading
Loading