Skip to content
Open
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
74 changes: 69 additions & 5 deletions app/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ fn update(
Ok(())
}

/// A block that is ready to be blind merged mined
pub struct BlockTemplate {
/// Bribe to offer for this block
pub bribe: bitcoin::Amount,
pub header: types::Header,
pub body: types::Body,
pub height: u32,
/// Fees collected by the transactions in the block
pub fees: bitcoin::Amount,
}

#[derive(Clone)]
pub struct App {
pub node: Arc<Node>,
Expand Down Expand Up @@ -340,10 +351,11 @@ impl App {
const EMPTY_BLOCK_BMM_BRIBE: bitcoin::Amount =
bitcoin::Amount::from_sat(1000);

pub async fn mine(
/// Assemble a block to blind merge mine, without requesting BMM for it
async fn build_block_template(
&self,
fee: Option<bitcoin::Amount>,
) -> Result<(), Error> {
) -> Result<BlockTemplate, Error> {
let Some(miner) = self.miner.as_ref() else {
return Err(Error::NoCusfMainchainWalletClient);
};
Expand Down Expand Up @@ -415,7 +427,7 @@ impl App {
} else {
None
};
let (bribe, header, body) = if prev_side_hash == tip_hash {
let (bribe, header, body, fees) = if prev_side_hash == tip_hash {
const NUM_TRANSACTIONS: usize = 1000;
let (txs, tx_fees) =
self.node.get_transactions(NUM_TRANSACTIONS)?;
Expand Down Expand Up @@ -450,7 +462,7 @@ impl App {
Self::EMPTY_BLOCK_BMM_BRIBE
}
});
(bribe, header, body)
(bribe, header, body, tx_fees)
} else {
let coinbase = Vec::new();
let merkle_root = Body::compute_merkle_root(&coinbase, &[]);
Expand All @@ -461,8 +473,60 @@ impl App {
prev_main_hash,
};
let bribe = Self::EMPTY_BLOCK_BMM_BRIBE;
(bribe, header, body)
(bribe, header, body, bitcoin::Amount::ZERO)
};
let height = match prev_side_hash {
None => 0,
Some(prev_side_hash) => self.node.get_height(prev_side_hash)? + 1,
};
Ok(BlockTemplate {
bribe,
header,
body,
height,
fees,
})
}

/// Assemble a block to blind merge mine. The caller requests BMM for
/// `header.hash()` itself, and submits the block via `connect_block` once
/// its BMM request is included in a mainchain block.
pub async fn get_block_template(&self) -> Result<BlockTemplate, Error> {
self.build_block_template(None).await
}

/// Connect a block for which a BMM request was included in the specified
/// mainchain block. Returns `true` if it was accepted as the new tip.
pub async fn connect_block(
&self,
block: types::Block,
main_block_hash: bitcoin::BlockHash,
) -> Result<bool, Error> {
let types::Block { header, body, .. } = block;
let accepted = self
.node
.submit_block(main_block_hash, &header, &body)
.await?;
if accepted {
let () = self.update()?;
}
Ok(accepted)
}

/// Attempt to mine a sidechain block
pub async fn mine(
&self,
fee: Option<bitcoin::Amount>,
) -> Result<(), Error> {
let Some(miner) = self.miner.as_ref() else {
return Err(Error::NoCusfMainchainWalletClient);
};
let BlockTemplate {
bribe,
header,
body,
..
} = self.build_block_template(fee).await?;
let mut miner_write = miner.write().await;
miner_write
.attempt_bmm(bribe.to_sat(), 0, header, body)
Expand Down
46 changes: 45 additions & 1 deletion app/rpc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ use plain_bitassets::{
},
wallet::Balance,
};
use plain_bitassets_app_rpc_api::{RpcServer, TxInfo};
use plain_bitassets_app_rpc_api::{
GetBlockTemplateResponse, RpcServer, TxInfo,
};
use tower_http::{
cors::CorsLayer,
request_id::{
Expand Down Expand Up @@ -172,6 +174,25 @@ impl RpcServer for RpcServerImpl {
self.app.wallet.get_bitcoin_balance().map_err(custom_err)
}

async fn connect_block(
&self,
block: Block,
main_block_hash: bitcoin::BlockHash,
) -> RpcResult<bool> {
self.app
.local_pool
.spawn_pinned({
let app = self.app.clone();
move || async move {
app.connect_block(block, main_block_hash)
.await
.map_err(custom_err)
}
})
.await
.unwrap()
}

async fn connect_peer(&self, addr: SocketAddr) -> RpcResult<()> {
self.app.node.connect_peer(addr).map_err(custom_err)
}
Expand Down Expand Up @@ -373,6 +394,29 @@ impl RpcServer for RpcServerImpl {
Ok(block)
}

async fn get_block_template(&self) -> RpcResult<GetBlockTemplateResponse> {
let template = self
.app
.local_pool
.spawn_pinned({
let app = self.app.clone();
move || async move {
app.get_block_template().await.map_err(custom_err)
}
})
.await
.unwrap()?;
Ok(GetBlockTemplateResponse {
critical_hash: template.header.hash(),
block: Block {
header: template.header,
body: template.body,
height: template.height,
},
fees_sats: template.fees.to_sat(),
})
}

async fn get_best_sidechain_block_hash(
&self,
) -> RpcResult<Option<BlockHash>> {
Expand Down
21 changes: 21 additions & 0 deletions cli/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ pub enum Command {
Bitassets,
/// Get Bitcoin balance in sats
BitcoinBalance,
/// Connect a block for which a BMM request was included in the specified
/// mainchain block. The block is the JSON returned by `get-block-template`.
ConnectBlock {
block: String,
main_block_hash: bitcoin::BlockHash,
},
/// Connect to a peer
ConnectPeer {
addr: SocketAddr,
Expand Down Expand Up @@ -141,6 +147,8 @@ pub enum Command {
GetBlock {
block_hash: BlockHash,
},
/// Assemble a block to blind merge mine, without requesting BMM for it
GetBlockTemplate,
/// Get the current block count
GetBlockcount,
/// Get mainchain blocks that commit to a specified block hash
Expand Down Expand Up @@ -362,6 +370,15 @@ where
let balance = rpc_client.bitcoin_balance().await?;
serde_json::to_string_pretty(&balance)?
}
Command::ConnectBlock {
block,
main_block_hash,
} => {
let block = serde_json::from_str(&block)?;
let accepted =
rpc_client.connect_block(block, main_block_hash).await?;
format!("{accepted}")
}
Command::ConnectPeer { addr } => {
let () = rpc_client.connect_peer(addr).await?;
String::default()
Expand Down Expand Up @@ -439,6 +456,10 @@ where
let block = rpc_client.get_block(block_hash).await?;
serde_json::to_string_pretty(&block)?
}
Command::GetBlockTemplate => {
let template = rpc_client.get_block_template().await?;
serde_json::to_string_pretty(&template)?
}
Command::GetBlockcount => {
let blockcount = rpc_client.getblockcount().await?;
format!("{blockcount}")
Expand Down
153 changes: 153 additions & 0 deletions integration_tests/block_template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Test assembling block templates

use bip300301_enforcer_integration_tests::{
integration_test::{activate_sidechain, fund_enforcer, propose_sidechain},
setup::{
Mode, Network, PostSetup as EnforcerPostSetup,
PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts,
Sidechain as _,
},
util::{AbortOnDrop, AsyncTrial, TestFailureCollector, TestFileRegistry},
};
use futures::{
FutureExt as _, StreamExt as _, channel::mpsc, future::BoxFuture,
};
use plain_bitassets_app_rpc_api::RpcClient as _;
use tokio::time::sleep;
use tracing::Instrument as _;

use crate::{
setup::{Init, PostSetup},
util::BinPaths,
};

/// Initial setup for the test
async fn setup(
bin_paths: &BinPaths,
res_tx: mpsc::UnboundedSender<anyhow::Result<()>>,
) -> anyhow::Result<EnforcerPostSetup> {
let enforcer_pre_setup =
EnforcerPreSetup::new(&bin_paths.others, Network::Regtest)?;
let mut enforcer_post_setup = {
let setup_opts: EnforcerSetupOpts = Default::default();
enforcer_pre_setup
.setup(Mode::Mempool, setup_opts, res_tx.clone())
.await?
};
let () = propose_sidechain::<PostSetup>(&mut enforcer_post_setup).await?;
tracing::info!("Proposed sidechain successfully");
let () = activate_sidechain::<PostSetup>(&mut enforcer_post_setup).await?;
tracing::info!("Activated sidechain successfully");
let () = fund_enforcer::<PostSetup>(&mut enforcer_post_setup).await?;
Ok(enforcer_post_setup)
}

async fn block_template_task(
bin_paths: BinPaths,
res_tx: mpsc::UnboundedSender<anyhow::Result<()>>,
) -> anyhow::Result<()> {
let mut enforcer_post_setup = setup(&bin_paths, res_tx.clone()).await?;
let sidechain = PostSetup::setup(
Init {
bitassets_app: bin_paths.bitassets()?.clone(),
data_dir_suffix: None,
},
&enforcer_post_setup,
res_tx,
)
.await?;
tracing::info!("Setup BitAssets node successfully");

tracing::debug!("Checking that the first template is empty");
let template = sidechain.rpc_client.get_block_template().await?;
anyhow::ensure!(template.block.header.prev_side_hash.is_none());
anyhow::ensure!(template.block.body.transactions.is_empty());
anyhow::ensure!(template.block.height == 0);
anyhow::ensure!(template.fees_sats == 0);

tracing::debug!("Checking that a template is stable while the chain is");
let template_repeat = sidechain.rpc_client.get_block_template().await?;
anyhow::ensure!(template_repeat.critical_hash == template.critical_hash);

tracing::debug!("Checking that a template is not connected by itself");
anyhow::ensure!(sidechain.rpc_client.getblockcount().await? == 0);

tracing::debug!("BMM 1 block");
let () = sidechain.bmm_single(&mut enforcer_post_setup).await?;
anyhow::ensure!(sidechain.rpc_client.getblockcount().await? == 1);

tracing::debug!("Checking that a template builds on the new tips");
let template_connected = sidechain.rpc_client.get_block_template().await?;
let best_side_hash =
sidechain.rpc_client.get_best_sidechain_block_hash().await?;
let best_main_hash =
sidechain.rpc_client.get_best_mainchain_block_hash().await?;
anyhow::ensure!(best_side_hash.is_some());
anyhow::ensure!(
template_connected.block.header.prev_side_hash == best_side_hash
);
anyhow::ensure!(
Some(template_connected.block.header.prev_main_hash) == best_main_hash
);
anyhow::ensure!(template_connected.block.height == 1);
anyhow::ensure!(
template_connected.critical_hash != template_repeat.critical_hash
);

tracing::debug!("Checking that a template commits to its own block");
anyhow::ensure!(
template_connected.block.header.hash()
== template_connected.critical_hash
);

tracing::debug!("Checking that a block without BMM is not connected");
let connect_without_bmm = sidechain
.rpc_client
.connect_block(
template_connected.block.clone(),
template_connected.block.header.prev_main_hash,
)
.await;
anyhow::ensure!(matches!(connect_without_bmm, Err(_) | Ok(false)));
anyhow::ensure!(sidechain.rpc_client.getblockcount().await? == 1);

drop(sidechain);
tracing::info!(
"Removing {}",
enforcer_post_setup.directories.base_dir.path().display()
);
drop(enforcer_post_setup.tasks);
// Wait for tasks to die
sleep(std::time::Duration::from_secs(1)).await;
enforcer_post_setup.directories.base_dir.cleanup()?;
Ok(())
}

async fn block_template(bin_paths: BinPaths) -> anyhow::Result<()> {
let (res_tx, mut res_rx) = mpsc::unbounded();
let _test_task: AbortOnDrop<()> = tokio::task::spawn({
let res_tx = res_tx.clone();
async move {
let res = block_template_task(bin_paths, res_tx.clone()).await;
let _send_err: Result<(), _> = res_tx.unbounded_send(res);
}
.in_current_span()
})
.into();
res_rx.next().await.ok_or_else(|| {
anyhow::anyhow!("Unexpected end of test task result stream")
})?
}

pub fn block_template_trial(
bin_paths: BinPaths,
file_registry: TestFileRegistry,
failure_collector: TestFailureCollector,
) -> AsyncTrial<BoxFuture<'static, anyhow::Result<()>>> {
AsyncTrial::new(
"block_template",
block_template(bin_paths).boxed(),
file_registry,
failure_collector,
)
}
Loading
Loading