Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved stacking orders #1331

Merged
merged 17 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 1 addition & 1 deletion components/clarinet-files/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use network_manifest::{
DEFAULT_BITCOIN_NODE_IMAGE, DEFAULT_DERIVATION_PATH, DEFAULT_DOCKER_PLATFORM,
DEFAULT_EPOCH_2_0, DEFAULT_EPOCH_2_05, DEFAULT_EPOCH_2_1, DEFAULT_EPOCH_2_2, DEFAULT_EPOCH_2_3,
DEFAULT_EPOCH_2_4, DEFAULT_EPOCH_2_5, DEFAULT_EPOCH_3_0, DEFAULT_FAUCET_MNEMONIC,
DEFAULT_POSTGRES_IMAGE, DEFAULT_STACKS_API_IMAGE, DEFAULT_STACKS_API_IMAGE_NAKA,
DEFAULT_FIRST_BURN_HEADER_HEIGHT, DEFAULT_POSTGRES_IMAGE, DEFAULT_STACKS_API_IMAGE,
DEFAULT_STACKS_EXPLORER_IMAGE, DEFAULT_STACKS_MINER_MNEMONIC, DEFAULT_STACKS_NODE_IMAGE,
DEFAULT_STACKS_NODE_IMAGE_NAKA, DEFAULT_SUBNET_API_IMAGE, DEFAULT_SUBNET_CONTRACT_ID,
DEFAULT_SUBNET_MNEMONIC, DEFAULT_SUBNET_NODE_IMAGE,
Expand Down
57 changes: 57 additions & 0 deletions components/clarinet-files/src/network_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub const DEFAULT_EPOCH_2_4: u64 = 104;
pub const DEFAULT_EPOCH_2_5: u64 = 105;
pub const DEFAULT_EPOCH_3_0: u64 = 121;

// Currently, the pox-4 contract has these values hardcoded:
// https://github.com/stacks-network/stacks-core/blob/e09ab931e2f15ff70f3bb5c2f4d7afb[…]42bd7bec6/stackslib/src/chainstate/stacks/boot/pox-testnet.clar
// but they may be configurable in the future.
pub const DEFAULT_POX_PREPARE_LENGTH: u64 = 4;
pub const DEFAULT_POX_REWARD_LENGTH: u64 = 10;
pub const DEFAULT_FIRST_BURN_HEADER_HEIGHT: u64 = 100;

#[derive(Serialize, Deserialize, Debug)]
pub struct NetworkManifestFile {
network: NetworkConfigFile,
Expand Down Expand Up @@ -316,6 +323,7 @@ pub struct PoxStackingOrder {
pub wallet: String,
pub slots: u64,
pub btc_address: String,
pub auto_extend: Option<bool>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -770,6 +778,34 @@ impl NetworkManifest {
let remapped_subnet_contract_id =
format!("{}.{}", default_deployer.stx_address, contract_id.name);

// validate that epoch 3.0 is started in a reward phase
let epoch_3_0 = devnet_config.epoch_3_0.unwrap_or(DEFAULT_EPOCH_3_0);
if !is_in_reward_phase(
DEFAULT_FIRST_BURN_HEADER_HEIGHT,
DEFAULT_POX_REWARD_LENGTH,
DEFAULT_POX_PREPARE_LENGTH,
&epoch_3_0,
) {
return Err(format!(
"Epoch 3.0 must start *during* a reward phase, not a prepare phase. Epoch 3.0 start set to: {}. Reward Cycle Length: {}. Prepare Phase Length: {}",
epoch_3_0, DEFAULT_POX_REWARD_LENGTH, DEFAULT_POX_PREPARE_LENGTH
));
}

// for stacking orders, we validate that wallet names match one of the provided accounts
if let Some(ref val) = devnet_config.pox_stacking_orders {
for (i, stacking_order) in val.iter().enumerate() {
let wallet_name = &stacking_order.wallet;
let wallet_is_in_accounts = accounts
.iter()
.any(|(account_name, _)| wallet_name == account_name);
if !wallet_is_in_accounts {
return Err(format!("Account data was not provided for the wallet ({}) listed in stacking order {}.", wallet_name, i + 1));
};
}

devnet_config.pox_stacking_orders = Some(val.clone());
}
let config = DevnetConfig {
name: devnet_config.name.take().unwrap_or("devnet".into()),
network_id: devnet_config.network_id,
Expand Down Expand Up @@ -1008,6 +1044,27 @@ fn compute_btc_address(public_key: &PublicKey, network: &BitcoinNetwork) -> Stri
btc_address.to_string()
}

// This logic was taken from stacks-core:
// https://github.com/stacks-network/stacks-core/blob/524b0e1ae9ad3c8d2d2ac37e72be4aee2c045ef8/src/burnchains/mod.rs#L513C30-L530
pub fn is_in_reward_phase(
first_block_height: u64,
reward_cycle_length: u64,
prepare_length: u64,
block_height: &u64,
) -> bool {
if block_height <= &first_block_height {
// not a reward cycle start if we're the first block after genesis.
false
} else {
let effective_height = block_height - first_block_height;
let reward_index = effective_height % reward_cycle_length;

// NOTE: first block in reward cycle is mod 1, so mod 0 is the last block in the
// prepare phase.
!(reward_index == 0 || reward_index > (reward_cycle_length - prepare_length))
}
}

#[cfg(feature = "wasm")]
fn compute_btc_address(_public_key: &PublicKey, _network: &BitcoinNetwork) -> String {
"__not_implemented__".to_string()
Expand Down
1 change: 1 addition & 0 deletions components/stacks-devnet-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ impl StacksDevnet {
wallet,
slots,
btc_address,
auto_extend: Some(false),
});
}
overrides.pox_stacking_orders = Some(stacking_orders);
Expand Down
203 changes: 115 additions & 88 deletions components/stacks-network/src/chains_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ use chainhook_sdk::observer::{
use chainhook_sdk::types::BitcoinBlockSignaling;
use chainhook_sdk::types::BitcoinChainEvent;
use chainhook_sdk::types::BitcoinNetwork;
use chainhook_sdk::types::StacksBlockData;
use chainhook_sdk::types::StacksChainEvent;
use chainhook_sdk::types::StacksNetwork;
use chainhook_sdk::types::StacksNodeConfig;
use chainhook_sdk::types::StacksTransactionKind;
use chainhook_sdk::utils::Context;
use clarinet_deployments::onchain::TransactionStatus;
use clarinet_deployments::onchain::{
apply_on_chain_deployment, DeploymentCommand, DeploymentEvent,
};
use clarinet_deployments::types::DeploymentSpecification;
use clarinet_files::PoxStackingOrder;
use clarinet_files::DEFAULT_FIRST_BURN_HEADER_HEIGHT;
use clarinet_files::{self, AccountConfig, DevnetConfig, NetworkManifest, ProjectManifest};
use clarity_repl::clarity::address::AddressHashMode;
use clarity_repl::clarity::util::hash::{hex_bytes, Hash160};
Expand Down Expand Up @@ -285,9 +284,29 @@ pub async fn start_chains_coordinator(
let (log, status) = match &chain_update {
BitcoinChainEvent::ChainUpdatedWithBlocks(event) => {
let tip = event.new_blocks.last().unwrap();
let log = format!("Bitcoin block #{} received", tip.block_identifier.index);
let bitcoin_block_height = tip.block_identifier.index;
let log = format!("Bitcoin block #{} received", bitcoin_block_height);
let status =
format!("mining blocks (chaintip = #{})", tip.block_identifier.index);
format!("mining blocks (chaintip = #{})", bitcoin_block_height);

if bitcoin_block_height > DEFAULT_FIRST_BURN_HEADER_HEIGHT + 1 {
let res = publish_stacking_orders(
&config.devnet_config,
&devnet_event_tx,
&config.accounts,
&config.services_map_hosts,
config.deployment_fee_rate,
bitcoin_block_height as u32,
)
.await;
if let Some(tx_count) = res {
let _ = devnet_event_tx.send(DevnetEvent::success(format!(
"Broadcasted {} stacking orders",
tx_count
)));
}
}

(log, status)
}
BitcoinChainEvent::ChainUpdatedWithReorg(events) => {
Expand Down Expand Up @@ -382,42 +401,6 @@ pub async fn start_chains_coordinator(
)
};
let _ = devnet_event_tx.send(DevnetEvent::info(message));

// only publish stacking order txs in tenure-change blocks
let has_coinbase_tx = known_tip
.block
.transactions
.iter()
.any(|tx| tx.metadata.kind == StacksTransactionKind::Coinbase);
if has_coinbase_tx {
let bitcoin_block_height = known_tip
.block
.metadata
.bitcoin_anchor_block_identifier
.index;

// stacking early in the cycle to make sure that
// the transactions are included in the next cycle
let should_submit_pox_orders = known_tip.block.metadata.pox_cycle_position == 1;
if should_submit_pox_orders {
let res = publish_stacking_orders(
&known_tip.block,
&config.devnet_config,
&devnet_event_tx,
&config.accounts,
&config.services_map_hosts,
config.deployment_fee_rate,
bitcoin_block_height as u32,
)
.await;
if let Some(tx_count) = res {
let _ = devnet_event_tx.send(DevnetEvent::success(format!(
"Broadcasted {} stacking orders",
tx_count
)));
}
}
}
}
ObserverEvent::NotifyBitcoinTransactionProxied => {
if !boot_completed.load(Ordering::SeqCst) {
Expand Down Expand Up @@ -542,38 +525,67 @@ pub fn relay_devnet_protocol_deployment(
}

pub async fn publish_stacking_orders(
block: &StacksBlockData,
devnet_config: &DevnetConfig,
devnet_event_tx: &Sender<DevnetEvent>,
accounts: &[AccountConfig],
services_map_hosts: &ServicesMapHosts,
fee_rate: u64,
bitcoin_block_height: u32,
) -> Option<usize> {
let orders_to_broadcast: Vec<&PoxStackingOrder> = devnet_config
.pox_stacking_orders
.iter()
.filter(|pox_stacking_order| {
pox_stacking_order.start_at_cycle - 1 == block.metadata.pox_cycle_index
})
.collect();

if orders_to_broadcast.is_empty() {
return None;
}

let stacks_node_rpc_url = format!("http://{}", &services_map_hosts.stacks_node_host);
let pox_info: PoxInfo = match reqwest::get(format!("{}/v2/pox", stacks_node_rpc_url)).await {
Ok(result) => match result.json().await {
Ok(pox_info) => Some(pox_info),
Err(e) => {
// Ignore errors for the first blocks, the api is not ready yet
if bitcoin_block_height < 110 {
hugocaillard marked this conversation as resolved.
Show resolved Hide resolved
let _ = devnet_event_tx.send(DevnetEvent::warning(format!(
"Unable to parse pox info: {}",
e
)));
};
None
}
},
Err(e) => {
let _ = devnet_event_tx.send(DevnetEvent::warning(format!(
"unable to retrieve pox info: {}",
e
)));
None
}
}?;

let mut transactions = 0;
let effective_height = u64::saturating_sub(
bitcoin_block_height.into(),
pox_info.first_burnchain_block_height,
);
let pox_cycle_length: u64 =
(pox_info.prepare_phase_block_length + pox_info.reward_phase_block_length).into();
let reward_cycle_id = effective_height / pox_cycle_length;

let pox_info: PoxInfo = reqwest::get(format!("{}/v2/pox", stacks_node_rpc_url))
.await
.expect("Unable to retrieve pox info")
.json()
.await
.expect("Unable to parse contract");
let pox_cycle_position = (effective_height % pox_cycle_length) as u32;

for (i, pox_stacking_order) in orders_to_broadcast.iter().enumerate() {
let should_submit_pox_orders = pox_cycle_position == 1;
if !should_submit_pox_orders {
return None;
}

let mut transactions = 0;
for (i, pox_stacking_order) in devnet_config.pox_stacking_orders.iter().enumerate() {
let PoxStackingOrder {
duration,
start_at_cycle,
..
} = pox_stacking_order;

if ((reward_cycle_id as u32) % duration) != (start_at_cycle - 1) {
continue;
}
let extend_stacking = reward_cycle_id as u32 != start_at_cycle - 1;
hugocaillard marked this conversation as resolved.
Show resolved Hide resolved
if extend_stacking && !pox_stacking_order.auto_extend.unwrap_or_default() {
continue;
}
let account = accounts
.iter()
.find(|e| e.label == pox_stacking_order.wallet);
Expand All @@ -590,7 +602,6 @@ pub async fn publish_stacking_orders(
.btc_address
.from_base58()
.expect("Unable to get bytes from btc address");
let duration = pox_stacking_order.duration.into();
let node_url = stacks_node_rpc_url.clone();
let pox_contract_id = pox_info.contract_id.clone();
let pox_version = pox_contract_id
Expand All @@ -599,6 +610,7 @@ pub async fn publish_stacking_orders(
.and_then(|version| version.parse::<u32>().ok())
.unwrap();

let duration = *duration;
let stacking_result =
hiro_system_kit::thread_named("Stacking orders handler").spawn(move || {
let default_fee = fee_rate * 1000;
Expand All @@ -611,43 +623,58 @@ pub async fn publish_stacking_orders(
&StacksNetwork::Devnet.get_networks(),
);

let addr_bytes = Hash160::from_bytes(&addr_bytes[1..21]).unwrap();
let addr_version = AddressHashMode::SerializeP2PKH;

let mut arguments = vec![
ClarityValue::UInt(stx_amount.into()),
ClarityValue::Tuple(
TupleData::from_data(vec![
(
ClarityName::try_from("version".to_owned()).unwrap(),
ClarityValue::buff_from_byte(addr_version as u8),
),
(
ClarityName::try_from("hashbytes".to_owned()).unwrap(),
ClarityValue::Sequence(SequenceData::Buffer(BuffData {
data: addr_bytes.as_bytes().to_vec(),
})),
),
])
.unwrap(),
let pox_addr_arg = ClarityValue::Tuple(
TupleData::from_data(vec![
(
ClarityName::try_from("version".to_owned()).unwrap(),
ClarityValue::buff_from_byte(AddressHashMode::SerializeP2PKH as u8),
),
(
ClarityName::try_from("hashbytes".to_owned()).unwrap(),
ClarityValue::Sequence(SequenceData::Buffer(BuffData {
data: Hash160::from_bytes(&addr_bytes[1..21])
.unwrap()
.as_bytes()
.to_vec(),
})),
),
])
.unwrap(),
);

let (method, mut arguments) = match extend_stacking {
false => (
"stack-stx",
vec![
ClarityValue::UInt(stx_amount.into()),
pox_addr_arg,
ClarityValue::UInt((bitcoin_block_height - 1).into()),
ClarityValue::UInt(duration.into()),
],
),
ClarityValue::UInt((bitcoin_block_height - 1).into()),
ClarityValue::UInt(duration),
];
true => (
"stack-extend",
vec![ClarityValue::UInt(duration.into()), pox_addr_arg],
),
};

if pox_version >= 4 {
let signer_key = vec![i as u8; 33];
let mut signer_key = vec![0; 33];
signer_key[0] = i as u8;
signer_key[1] = nonce as u8;
arguments.push(ClarityValue::buff_from(signer_key).unwrap());
};

let stack_stx_tx = codec::build_contrat_call_transaction(
let tx = codec::build_contrat_call_transaction(
pox_contract_id,
"stack-stx".into(),
method.into(),
arguments,
nonce,
default_fee,
&hex_bytes(&account_secret_key).unwrap(),
);
stacks_rpc.post_transaction(&stack_stx_tx)

stacks_rpc.post_transaction(&tx)
});

match stacking_result {
Expand Down
2 changes: 1 addition & 1 deletion components/stacks-network/src/ui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ fn draw_block_details(f: &mut Frame, area: Rect, block: &StacksBlockData) {
Paragraph::new("PoX informations").style(Style::default().add_modifier(Modifier::BOLD));
f.render_widget(title, labels[7]);

let label = format!("PoX Cycle: {}", block.metadata.pox_cycle_index);
let label = format!("PoX Cycle: {}", block.metadata.pox_cycle_index + 1);
let paragraph = Paragraph::new(label);
f.render_widget(paragraph, labels[8]);

Expand Down
Loading
Loading