Revisit testnet module structure

This commit is contained in:
Ludo Galabru
2020-04-14 19:19:00 -04:00
parent ba72993856
commit e2c449f08f
23 changed files with 314 additions and 897 deletions

View File

@@ -1,2 +1,2 @@
[alias]
testnet = "run --package stacks-helium --"
testnet = "run --package stacks-testnet --"

View File

@@ -85,4 +85,4 @@ default = ["developer-mode", "asm"]
[workspace]
members = [".", "testnet/helium/"]
members = [".", "testnet/"]

View File

@@ -1,745 +0,0 @@
use vm::{
database::{ HeadersDB, ClaritySerializable },
types::{QualifiedContractIdentifier, TupleData, PrincipalData},
analysis::{mem_type_check, contract_interface_builder::{build_contract_interface, ContractInterface}},
clarity::ClarityConnection,
Value, ClarityName, ContractName, errors::RuntimeErrorType, errors::Error as ClarityError };
use chainstate::stacks::{
db::StacksChainState, C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
StacksMicroblockHeader, StacksPrivateKey, TransactionSpendingCondition, TransactionAuth, TransactionVersion,
StacksPublicKey, TransactionPayload, StacksTransactionSigner,
TokenTransferMemo, CoinbasePayload, TransactionPostConditionMode,
StacksTransaction, TransactionSmartContract, TransactionContractCall, StacksAddress };
use chainstate::burn::VRFSeed;
use burnchains::Address;
use address::AddressHashMode;
use net::{Error as NetError, StacksMessageCodec, AccountEntryResponse, ContractSrcResponse, CallReadOnlyRequestBody};
use util::{log, strings::StacksString, hash::hex_bytes, hash::to_hex};
use std::collections::HashMap;
use util::db::{DBConn, FromRow};
use std::{thread, time};
use testnet;
use testnet::helium::{
mem_pool::MemPool,
config::InitialBalance
};
use reqwest;
pub fn serialize_sign_standard_single_sig_tx(payload: TransactionPayload,
sender: &StacksPrivateKey, nonce: u64, fee_rate: u64) -> Vec<u8> {
let mut spending_condition = TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private(sender))
.expect("Failed to create p2pkh spending condition from public key.");
spending_condition.set_nonce(nonce);
spending_condition.set_fee_rate(fee_rate);
let auth = TransactionAuth::Standard(spending_condition);
let mut unsigned_tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload);
unsigned_tx.post_condition_mode = TransactionPostConditionMode::Allow;
let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx);
tx_signer.sign_origin(sender).unwrap();
let mut buf = vec![];
tx_signer.get_tx().unwrap().consensus_serialize(&mut buf).unwrap();
buf
}
pub fn make_contract_publish(sender: &StacksPrivateKey, nonce: u64, fee_rate: u64,
contract_name: &str, contract_content: &str) -> Vec<u8> {
let name = ContractName::from(contract_name);
let code_body = StacksString::from_string(&contract_content.to_string()).unwrap();
let payload = TransactionSmartContract { name, code_body };
serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate)
}
pub fn make_stacks_transfer(sender: &StacksPrivateKey, nonce: u64, fee_rate: u64,
recipient: &PrincipalData, amount: u64) -> Vec<u8> {
let payload = TransactionPayload::TokenTransfer(recipient.clone(), amount, TokenTransferMemo([0; 34]));
serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate)
}
pub fn make_poison(sender: &StacksPrivateKey, nonce: u64, fee_rate: u64,
header_1: StacksMicroblockHeader, header_2: StacksMicroblockHeader) -> Vec<u8> {
let payload = TransactionPayload::PoisonMicroblock(header_1, header_2);
serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate)
}
pub fn make_coinbase(sender: &StacksPrivateKey, nonce: u64, fee_rate: u64) -> Vec<u8> {
let payload = TransactionPayload::Coinbase(CoinbasePayload([0; 32]));
serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate)
}
pub fn make_contract_call(
sender: &StacksPrivateKey, nonce: u64, fee_rate: u64,
contract_addr: &StacksAddress, contract_name: &str,
function_name: &str, function_args: &[Value]) -> Vec<u8> {
let contract_name = ContractName::from(contract_name);
let function_name = ClarityName::from(function_name);
let payload = TransactionContractCall {
address: contract_addr.clone(),
contract_name, function_name,
function_args: function_args.iter().map(|x| x.clone()).collect()
};
serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate)
}
pub fn to_addr(sk: &StacksPrivateKey) -> StacksAddress {
StacksAddress::from_public_keys(
C32_ADDRESS_VERSION_TESTNET_SINGLESIG, &AddressHashMode::SerializeP2PKH, 1, &vec![StacksPublicKey::from_private(sk)])
.unwrap()
}
const GET_INFO_CONTRACT: &'static str = "
(define-map block-data
((height uint))
((stacks-hash (buff 32))
(id-hash (buff 32))
(btc-hash (buff 32))
(vrf-seed (buff 32))
(burn-block-time uint)
(stacks-miner principal)))
(define-private (test-1) (get-block-info? time u1))
(define-private (test-2) (get-block-info? time block-height))
(define-private (test-3) (get-block-info? time u100000))
(define-private (test-4 (x uint)) (get-block-info? header-hash x))
(define-private (test-5) (get-block-info? header-hash (- block-height u1)))
(define-private (test-6) (get-block-info? burnchain-header-hash u1))
(define-private (test-7) (get-block-info? vrf-seed u1))
(define-private (test-8) (get-block-info? miner-address u1))
(define-private (test-9) (get-block-info? miner-address block-height))
(define-private (test-10) (get-block-info? miner-address u100000))
(define-private (get-block-id-hash (height uint)) (unwrap-panic
(get id-hash (map-get? block-data ((height height))))))
;; should always return true!
;; evaluates 'block-height' at the block in question.
;; NOTABLY, this would fail if the MARF couldn't figure out
;; the height of the 'current chain tip'.
(define-private (exotic-block-height (height uint))
(is-eq (at-block (get-block-id-hash height) block-height)
height))
(define-read-only (get-exotic-data-info (height uint))
(unwrap-panic (map-get? block-data { height: height })))
(define-private (exotic-data-checks (height uint))
(let ((block-to-check (unwrap-panic (get-block-info? id-header-hash height)))
(block-info (unwrap-panic (map-get? block-data ((height (- height u1)))))))
(and (is-eq (print (unwrap-panic (at-block block-to-check (get-block-info? id-header-hash (- block-height u1)))))
(print (get id-hash block-info)))
(is-eq (print (unwrap-panic (at-block block-to-check (get-block-info? header-hash (- block-height u1)))))
(print (unwrap-panic (get-block-info? header-hash (- height u1))))
(print (get stacks-hash block-info)))
(is-eq (print (unwrap-panic (at-block block-to-check (get-block-info? vrf-seed (- block-height u1)))))
(print (unwrap-panic (get-block-info? vrf-seed (- height u1))))
(print (get vrf-seed block-info)))
(is-eq (print (unwrap-panic (at-block block-to-check (get-block-info? burnchain-header-hash (- block-height u1)))))
(print (unwrap-panic (get-block-info? burnchain-header-hash (- height u1))))
(print (get btc-hash block-info)))
(is-eq (print (unwrap-panic (at-block block-to-check (get-block-info? time (- block-height u1)))))
(print (unwrap-panic (get-block-info? time (- height u1))))
(print (get burn-block-time block-info)))
(is-eq (print (unwrap-panic (at-block block-to-check (get-block-info? miner-address (- block-height u1)))))
(print (unwrap-panic (get-block-info? miner-address (- height u1))))
(print (get stacks-miner block-info))))))
(define-private (inner-update-info (height uint))
(let ((value (tuple
(stacks-hash (unwrap-panic (get-block-info? header-hash height)))
(id-hash (unwrap-panic (get-block-info? id-header-hash height)))
(btc-hash (unwrap-panic (get-block-info? burnchain-header-hash height)))
(vrf-seed (unwrap-panic (get-block-info? vrf-seed height)))
(burn-block-time (unwrap-panic (get-block-info? time height)))
(stacks-miner (unwrap-panic (get-block-info? miner-address height))))))
(ok (map-set block-data ((height height)) value))))
(define-public (update-info)
(begin
(inner-update-info (- block-height u2))
(inner-update-info (- block-height u1))))
";
const SK_1: &'static str = "a1289f6438855da7decf9b61b852c882c398cff1446b2a0f823538aa2ebef92e01";
const SK_2: &'static str = "4ce9a8f7539ea93753a36405b16e8b57e15a552430410709c2b6d65dca5c02e201";
const SK_3: &'static str = "cb95ddd0fe18ec57f4f3533b95ae564b3f1ae063dbf75b46334bd86245aef78501";
const ADDR_4: &'static str = "SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0";
use std::sync::Mutex;
lazy_static! {
static ref http_binding: Mutex<Option<String>> = Mutex::new(None);
}
#[test]
fn integration_test_get_info() {
let mut conf = testnet::helium::tests::new_test_conf();
let spender_addr = to_addr(&StacksPrivateKey::from_hex(SK_3).unwrap()).into();
conf.initial_balances.push(InitialBalance {
address: spender_addr,
amount: 100300
});
conf.burnchain.block_time = 1500;
let num_rounds = 4;
let mut run_loop = testnet::helium::RunLoop::new(conf);
{
let mut http_opt = http_binding.lock().unwrap();
http_opt.replace(format!("http://{}", &run_loop.node.config.node.rpc_bind));
}
run_loop.apply_on_new_tenures(|round, tenure| {
let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
let principal_sk = StacksPrivateKey::from_hex(SK_2).unwrap();
let spender_sk = StacksPrivateKey::from_hex(SK_3).unwrap();
if round == 1 { // block-height = 2
let publish_tx = make_contract_publish(&contract_sk, 0, 0, "get-info", GET_INFO_CONTRACT);
eprintln!("Tenure in 1 started!");
tenure.mem_pool.submit(publish_tx);
} else if round >= 2 { // block-height > 2
let tx = make_contract_call(&principal_sk, (round - 2).into(), 0, &to_addr(&contract_sk), "get-info", "update-info", &[]);
tenure.mem_pool.submit(tx);
}
if round >= 1 {
let tx_xfer = make_stacks_transfer(&spender_sk, (round - 1).into(), 0,
&StacksAddress::from_string(ADDR_4).unwrap().into(), 100);
tenure.mem_pool.submit(tx_xfer);
}
return
});
run_loop.apply_on_new_chain_states(|round, chain_state, block, chain_tip_info, _events| {
let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap());
let contract_identifier =
QualifiedContractIdentifier::parse(&format!("{}.{}", &contract_addr, "get-info")).unwrap();
let http_origin = {
http_binding.lock().unwrap().clone().unwrap()
};
match round {
1 => {
// - Chain length should be 2.
let mut blocks = StacksChainState::list_blocks(&chain_state.blocks_db).unwrap();
blocks.sort();
assert!(chain_tip_info.block_height == 2);
// Block #1 should have 3 txs
assert!(block.txs.len() == 3);
let parent = block.header.parent_block;
let bhh = &chain_tip_info.index_block_hash();
eprintln!("Current Block: {} Parent Block: {}", bhh, parent);
let parent_val = Value::buff_from(parent.as_bytes().to_vec()).unwrap();
// find header metadata
let mut headers = vec![];
for block in blocks.iter() {
let header = StacksChainState::get_anchored_block_header_info(&chain_state.headers_db, &block.0, &block.1).unwrap().unwrap();
headers.push(header);
}
let _tip_header_info = headers.last().unwrap();
// find miner metadata
let mut miners = vec![];
for block in blocks.iter() {
let miner = StacksChainState::get_miner_info(&chain_state.headers_db, &block.0, &block.1).unwrap().unwrap();
miners.push(miner);
}
let _tip_miner = miners.last().unwrap();
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "block-height"),
Value::UInt(2));
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-1)"),
Value::some(Value::UInt(headers[0].burn_header_timestamp as u128)).unwrap());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-2)"),
Value::none());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-3)"),
Value::none());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-4 u1)"),
Value::some(parent_val.clone()).unwrap());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-5)"),
Value::some(parent_val).unwrap());
// test-6 and test-7 return the block at height 1's VRF-seed,
// which in this integration test, should be blocks[0]
let last_tip = blocks[0];
eprintln!("Last block info: stacks: {}, burn: {}", last_tip.1, last_tip.0);
let last_block = StacksChainState::load_block(&chain_state.blocks_path, &last_tip.0, &last_tip.1).unwrap().unwrap();
assert_eq!(parent, last_block.header.block_hash());
let last_vrf_seed = VRFSeed::from_proof(&last_block.header.proof).as_bytes().to_vec();
let last_burn_header = last_tip.0.as_bytes().to_vec();
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-6)"),
Value::some(Value::buff_from(last_burn_header).unwrap()).unwrap());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-7)"),
Value::some(Value::buff_from(last_vrf_seed).unwrap()).unwrap());
// verify that we can get the block miner
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-8)"),
Value::some(Value::Principal(miners[0].address.to_account_principal())).unwrap());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-9)"),
Value::none());
assert_eq!(
chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(test-10)"),
Value::none());
},
3 => {
let bhh = &chain_tip_info.index_block_hash();
assert_eq!(Value::Bool(true), chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(exotic-block-height u1)"));
assert_eq!(Value::Bool(true), chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(exotic-block-height u2)"));
assert_eq!(Value::Bool(true), chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(exotic-block-height u3)"));
assert_eq!(Value::Bool(true), chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(exotic-data-checks u2)"));
assert_eq!(Value::Bool(true), chain_state.clarity_eval_read_only(
bhh, &contract_identifier, "(exotic-data-checks u3)"));
let client = reqwest::blocking::Client::new();
let path = format!("{}/v2/map_entry/{}/{}/{}",
&http_origin, &contract_addr, "get-info", "block-data");
let key: Value = TupleData::from_data(vec![("height".into(), Value::UInt(1))])
.unwrap().into();
eprintln!("Test: POST {}", path);
let res = client.post(&path)
.json(&key.serialize())
.send()
.unwrap().json::<HashMap<String, String>>().unwrap();
let result_data = Value::try_deserialize_hex_untyped(&res["data"][2..]).unwrap();
let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier,
"(some (get-exotic-data-info u1))");
assert!(res.get("proof").is_some());
assert_eq!(result_data, expected_data);
let key: Value = TupleData::from_data(vec![("height".into(), Value::UInt(100))])
.unwrap().into();
eprintln!("Test: POST {}", path);
let res = client.post(&path)
.json(&key.serialize())
.send()
.unwrap().json::<HashMap<String, String>>().unwrap();
let result_data = Value::try_deserialize_hex_untyped(&res["data"][2..]).unwrap();
assert_eq!(result_data, Value::none());
let sender_addr = to_addr(&StacksPrivateKey::from_hex(SK_3).unwrap());
// now, let's use a query string to get data without a proof
let path = format!("{}/v2/map_entry/{}/{}/{}?proof=0",
&http_origin, &contract_addr, "get-info", "block-data");
let key: Value = TupleData::from_data(vec![("height".into(), Value::UInt(1))])
.unwrap().into();
eprintln!("Test: POST {}", path);
let res = client.post(&path)
.json(&key.serialize())
.send()
.unwrap().json::<HashMap<String, String>>().unwrap();
assert!(res.get("proof").is_none());
let result_data = Value::try_deserialize_hex_untyped(&res["data"][2..]).unwrap();
let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier,
"(some (get-exotic-data-info u1))");
eprintln!("{}", serde_json::to_string(&res).unwrap());
assert_eq!(result_data, expected_data);
// now, let's use a query string to get data _with_ a proof
let path = format!("{}/v2/map_entry/{}/{}/{}?proof=1",
&http_origin, &contract_addr, "get-info", "block-data");
let key: Value = TupleData::from_data(vec![("height".into(), Value::UInt(1))])
.unwrap().into();
eprintln!("Test: POST {}", path);
let res = client.post(&path)
.json(&key.serialize())
.send()
.unwrap().json::<HashMap<String, String>>().unwrap();
assert!(res.get("proof").is_some());
let result_data = Value::try_deserialize_hex_untyped(&res["data"][2..]).unwrap();
let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier,
"(some (get-exotic-data-info u1))");
eprintln!("{}", serde_json::to_string(&res).unwrap());
assert_eq!(result_data, expected_data);
// account with a nonce entry + a balance entry
let path = format!("{}/v2/accounts/{}",
&http_origin, &sender_addr);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<AccountEntryResponse>().unwrap();
assert_eq!(u128::from_str_radix(&res.balance[2..], 16).unwrap(), 100000);
assert_eq!(res.nonce, 3);
assert!(res.nonce_proof.is_some());
assert!(res.balance_proof.is_some());
// account with a nonce entry but not a balance entry
let path = format!("{}/v2/accounts/{}",
&http_origin, &contract_addr);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<AccountEntryResponse>().unwrap();
assert_eq!(u128::from_str_radix(&res.balance[2..], 16).unwrap(), 0);
assert_eq!(res.nonce, 1);
assert!(res.nonce_proof.is_some());
assert!(res.balance_proof.is_some());
// account with a balance entry but not a nonce entry
let path = format!("{}/v2/accounts/{}",
&http_origin, ADDR_4);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<AccountEntryResponse>().unwrap();
assert_eq!(u128::from_str_radix(&res.balance[2..], 16).unwrap(), 300);
assert_eq!(res.nonce, 0);
assert!(res.nonce_proof.is_some());
assert!(res.balance_proof.is_some());
// account with neither!
let path = format!("{}/v2/accounts/{}.get-info",
&http_origin, &contract_addr);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<AccountEntryResponse>().unwrap();
assert_eq!(u128::from_str_radix(&res.balance[2..], 16).unwrap(), 0);
assert_eq!(res.nonce, 0);
assert!(res.nonce_proof.is_some());
assert!(res.balance_proof.is_some());
let path = format!("{}/v2/accounts/{}?proof=0",
&http_origin, ADDR_4);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<AccountEntryResponse>().unwrap();
assert_eq!(u128::from_str_radix(&res.balance[2..], 16).unwrap(), 300);
assert_eq!(res.nonce, 0);
assert!(res.nonce_proof.is_none());
assert!(res.balance_proof.is_none());
let path = format!("{}/v2/accounts/{}?proof=1",
&http_origin, ADDR_4);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<AccountEntryResponse>().unwrap();
assert_eq!(u128::from_str_radix(&res.balance[2..], 16).unwrap(), 300);
assert_eq!(res.nonce, 0);
assert!(res.nonce_proof.is_some());
assert!(res.balance_proof.is_some());
// let's try getting the transfer cost
let path = format!("{}/v2/fees/transfer", &http_origin);
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<u64>().unwrap();
assert!(res > 0);
// let's get a contract ABI
let path = format!("{}/v2/contracts/interface/{}/{}", &http_origin, &contract_addr, "get-info");
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<ContractInterface>().unwrap();
let contract_analysis = mem_type_check(GET_INFO_CONTRACT).unwrap().1;
let expected_interface = build_contract_interface(&contract_analysis);
eprintln!("{}", serde_json::to_string(&expected_interface).unwrap());
assert_eq!(res, expected_interface);
// a missing one?
let path = format!("{}/v2/contracts/interface/{}/{}", &http_origin, &contract_addr, "not-there");
eprintln!("Test: GET {}", path);
assert_eq!(client.get(&path).send().unwrap().status(), 404);
// let's get a contract SRC
let path = format!("{}/v2/contracts/source/{}/{}", &http_origin, &contract_addr, "get-info");
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<ContractSrcResponse>().unwrap();
assert_eq!(res.source, GET_INFO_CONTRACT);
assert_eq!(res.publish_height, 2);
assert!(res.marf_proof.is_some());
let path = format!("{}/v2/contracts/source/{}/{}?proof=0", &http_origin, &contract_addr, "get-info");
eprintln!("Test: GET {}", path);
let res = client.get(&path).send().unwrap().json::<ContractSrcResponse>().unwrap();
assert_eq!(res.source, GET_INFO_CONTRACT);
assert_eq!(res.publish_height, 2);
assert!(res.marf_proof.is_none());
// a missing one?
let path = format!("{}/v2/contracts/source/{}/{}", &http_origin, &contract_addr, "not-there");
eprintln!("Test: GET {}", path);
assert_eq!(client.get(&path).send().unwrap().status(), 404);
// how about a read-only function call!
let path = format!("{}/v2/contracts/call-read/{}/{}/{}", &http_origin, &contract_addr, "get-info", "get-exotic-data-info");
eprintln!("Test: POST {}", path);
let body = CallReadOnlyRequestBody {
sender: "'SP139Q3N9RXCJCD1XVA4N5RYWQ5K9XQ0T9PKQ8EE5".into(),
arguments: vec![Value::UInt(1).serialize()]
};
let res = client.post(&path)
.json(&body)
.send()
.unwrap().json::<serde_json::Value>().unwrap();
assert!(res.get("cause").is_none());
assert!(res["okay"].as_bool().unwrap());
let result_data = Value::try_deserialize_hex_untyped(&res["result"].as_str().unwrap()[2..]).unwrap();
let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier,
"(get-exotic-data-info u1)");
assert_eq!(result_data, expected_data);
// let's have a runtime error!
let path = format!("{}/v2/contracts/call-read/{}/{}/{}", &http_origin, &contract_addr, "get-info", "get-exotic-data-info");
eprintln!("Test: POST {}", path);
let body = CallReadOnlyRequestBody {
sender: "'SP139Q3N9RXCJCD1XVA4N5RYWQ5K9XQ0T9PKQ8EE5".into(),
arguments: vec![Value::UInt(100).serialize()]
};
let res = client.post(&path)
.json(&body)
.send()
.unwrap().json::<serde_json::Value>().unwrap();
assert!(res.get("result").is_none());
assert!(!res["okay"].as_bool().unwrap());
assert!(res["cause"].as_str().unwrap().contains("UnwrapFailure"));
// let's have a runtime error!
let path = format!("{}/v2/contracts/call-read/{}/{}/{}", &http_origin, &contract_addr, "get-info", "update-info");
eprintln!("Test: POST {}", path);
let body = CallReadOnlyRequestBody {
sender: "'SP139Q3N9RXCJCD1XVA4N5RYWQ5K9XQ0T9PKQ8EE5".into(),
arguments: vec![]
};
let res = client.post(&path)
.json(&body)
.send()
.unwrap().json::<serde_json::Value>().unwrap();
eprintln!("{}", res["cause"].as_str().unwrap());
assert!(res.get("result").is_none());
assert!(!res["okay"].as_bool().unwrap());
assert!(res["cause"].as_str().unwrap().contains("NotReadOnly"));
},
_ => {},
}
});
run_loop.start(num_rounds);
}
const FAUCET_CONTRACT: &'static str = "
(define-public (spout)
(let ((recipient tx-sender))
(print (as-contract (stx-transfer? u1 .faucet recipient)))))
";
#[test]
fn contract_stx_transfer() {
let mut conf = testnet::helium::tests::new_test_conf();
let sk_3 = StacksPrivateKey::from_hex(SK_3).unwrap();
let addr_3 = to_addr(&sk_3);
conf.burnchain.block_time = 1500;
conf.add_initial_balance(addr_3.to_string(), 100000);
let num_rounds = 5;
let mut run_loop = testnet::helium::RunLoop::new(conf);
run_loop.apply_on_new_tenures(|round, tenure| {
let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
let sk_2 = StacksPrivateKey::from_hex(SK_2).unwrap();
let sk_3 = StacksPrivateKey::from_hex(SK_3).unwrap();
let contract_identifier =
QualifiedContractIdentifier::parse(&format!("{}.{}",
to_addr(
&StacksPrivateKey::from_hex(SK_1).unwrap()).to_string(),
"faucet")).unwrap();
if round == 1 { // block-height = 2
let xfer_to_contract = make_stacks_transfer(&sk_3, 0, 0, &contract_identifier.into(), 1000);
tenure.mem_pool.submit(xfer_to_contract);
} else if round == 2 { // block-height > 2
let publish_tx = make_contract_publish(&contract_sk, 0, 0, "faucet", FAUCET_CONTRACT);
tenure.mem_pool.submit(publish_tx);
} else if round == 3 {
// try to publish again
// TODO: disabled, pending resolution of issue #1376
// let publish_tx = make_contract_publish(&contract_sk, 1, 0, "faucet", FAUCET_CONTRACT);
// tenure.mem_pool.submit(publish_tx);
let tx = make_contract_call(&sk_2, 0, 0, &to_addr(&contract_sk), "faucet", "spout", &[]);
tenure.mem_pool.submit(tx);
} else if round == 4 {
// transfer to the contract again.
let xfer_to_contract = make_stacks_transfer(&sk_3, 1, 0, &contract_identifier.into(), 1000);
tenure.mem_pool.submit(xfer_to_contract);
}
return
});
run_loop.apply_on_new_chain_states(|round, chain_state, block, chain_tip_info, _events| {
let contract_identifier =
QualifiedContractIdentifier::parse(&format!("{}.{}",
to_addr(
&StacksPrivateKey::from_hex(SK_1).unwrap()).to_string(),
"faucet")).unwrap();
match round {
1 => {
assert!(chain_tip_info.block_height == 2);
// Block #1 should have 2 txs -- coinbase + transfer
assert!(block.txs.len() == 2);
let cur_tip = (chain_tip_info.burn_header_hash.clone(), chain_tip_info.anchored_header.block_hash());
// check that 1000 stx _was_ transfered to the contract principal
assert_eq!(
chain_state.with_read_only_clarity_tx(&cur_tip.0, &cur_tip.1, |conn| {
conn.with_clarity_db_readonly(|db| {
db.get_account_stx_balance(&contract_identifier.clone().into())
})
}),
1000);
// check that 1000 stx _was_ debited from SK_3
let sk_3 = StacksPrivateKey::from_hex(SK_3).unwrap();
let addr_3 = to_addr(&sk_3).into();
assert_eq!(
chain_state.with_read_only_clarity_tx(&cur_tip.0, &cur_tip.1, |conn| {
conn.with_clarity_db_readonly(|db| {
db.get_account_stx_balance(&addr_3)
})
}),
99000);
},
2 => {
assert!(chain_tip_info.block_height == 3);
// Block #2 should have 2 txs -- coinbase + publish
assert!(block.txs.len() == 2);
},
3 => {
assert!(chain_tip_info.block_height == 4);
// Block #3 should have 2 txs -- coinbase + contract-call,
// the second publish _should have been rejected_
assert!(block.txs.len() == 2);
// check that 1 stx was transfered to SK_2 via the contract-call
let cur_tip = (chain_tip_info.burn_header_hash.clone(), chain_tip_info.anchored_header.block_hash());
let sk_2 = StacksPrivateKey::from_hex(SK_2).unwrap();
let addr_2 = to_addr(&sk_2).into();
assert_eq!(
chain_state.with_read_only_clarity_tx(&cur_tip.0, &cur_tip.1, |conn| {
conn.with_clarity_db_readonly(|db| {
db.get_account_stx_balance(&addr_2)
})
}),
1);
assert_eq!(
chain_state.with_read_only_clarity_tx(&cur_tip.0, &cur_tip.1, |conn| {
conn.with_clarity_db_readonly(|db| {
db.get_account_stx_balance(&contract_identifier.clone().into())
})
}),
999);
},
4 => {
assert!(chain_tip_info.block_height == 5);
assert!(block.txs.len() == 2);
let cur_tip = (chain_tip_info.burn_header_hash.clone(), chain_tip_info.anchored_header.block_hash());
// check that 1000 stx were sent to the contract
assert_eq!(
chain_state.with_read_only_clarity_tx(&cur_tip.0, &cur_tip.1, |conn| {
conn.with_clarity_db_readonly(|db| {
db.get_account_stx_balance(&contract_identifier.clone().into())
})
}),
1999);
// check that 1000 stx _was_ debited from SK_3
let sk_3 = StacksPrivateKey::from_hex(SK_3).unwrap();
let addr_3 = to_addr(&sk_3).into();
assert_eq!(
chain_state.with_read_only_clarity_tx(&cur_tip.0, &cur_tip.1, |conn| {
conn.with_clarity_db_readonly(|db| {
db.get_account_stx_balance(&addr_3)
})
}),
98000);
},
_ => {},
}
});
run_loop.start(num_rounds);
}

View File

@@ -1,5 +1,5 @@
[package]
name = "stacks-helium"
name = "stacks-testnet"
version = "0.1.0"
authors = ["Ludo Galabru <ludovic@blockstack.com>"]
edition = "2018"
@@ -13,9 +13,9 @@ secp256k1 = { version = "0.11.5" }
serde = "1"
serde_derive = "1"
serde_json = { version = "1.0", features = ["arbitrary_precision"] }
stacks = { package = "blockstack-core", path = "../../." }
stacks = { package = "blockstack-core", path = "../." }
toml = "0.5.6"
[[bin]]
name = "stacks-helium"
name = "stacks-testnet"
path = "src/main.rs"

View File

@@ -1,6 +1,7 @@
[node]
name = "helium-node"
rpc_bind = "127.0.0.1:9000"
working_dir = "/tmp/stacks-helium"
## Settings for local testnet, relying on a local bitcoind server
## running with the following bitcoin.conf:

View File

@@ -20,7 +20,6 @@ pub mod event_dispatcher;
pub mod operations;
pub mod burnchains;
pub use self::run_loop::{RunLoop};
pub use self::mem_pool::{MemPool, MemPoolFS};
pub use self::keychain::{Keychain};
pub use self::node::{Node, ChainTip};
@@ -28,6 +27,8 @@ pub use self::burnchains::{MocknetController, BitcoinRegtestController, Burnchai
pub use self::tenure::{Tenure};
pub use self::config::{Config, ConfigFile};
pub use self::event_dispatcher::{EventDispatcher};
pub use self::run_loop::{neon, helium};
use std::env;
@@ -50,9 +51,27 @@ fn main() {
println!("*** Mempool path: {}", conf.mempool.path);
let mut run_loop = RunLoop::new(conf);
// ^C2020-04-14T15:36:48Z tor: Thread interrupt
// 2020-04-14T15:36:48Z torcontrol thread exit
// 2020-04-14T15:36:48Z opencon thread exit
// 2020-04-14T15:36:48Z addcon thread exit
// 2020-04-14T15:36:48Z Shutdown: In progress...
// 2020-04-14T15:36:48Z net thread exit
// 2020-04-14T15:36:48Z msghand thread exit
// 2020-04-14T15:36:48Z scheduler thread interrupt
// 2020-04-14T15:36:48Z Dumped mempool: 9e-06s to copy, 0.002823s to dump
// 2020-04-14T15:36:48Z [default wallet] Releasing wallet
// 2020-04-14T15:36:48Z Shutdown: done
let num_round: u64 = 0; // Infinite number of rounds
run_loop.start(num_round);
if conf.burnchain.mode == "helium" {
let mut run_loop = helium::RunLoop::new(conf);
run_loop.start(num_round);
} else if conf.burnchain.mode == "neon" {
let mut run_loop = neon::RunLoop::new(conf);
run_loop.start(num_round);
}
return
}

View File

@@ -109,7 +109,7 @@ impl Node {
Some(initial_balances),
boot_block_exec) {
Ok(res) => res,
Err(_) => panic!("Error while opening chain state at path {:?}", config.get_chainstate_path())
Err(err) => panic!("Error while opening chain state at path {}: {:?}", config.get_chainstate_path(), err)
};
let mem_pool = MemPoolFS::new(&config.mempool.path);

View File

@@ -1,36 +1,15 @@
use super::{Config, Node, BurnchainController, MocknetController, BitcoinRegtestController, BurnchainTip, ChainTip, Tenure};
use crate::{Config, Node, BurnchainController, MocknetController, BitcoinRegtestController, ChainTip};
use stacks::chainstate::stacks::db::{StacksChainState, ClarityTx};
use stacks::chainstate::stacks::{TransactionAuth, TransactionSpendingCondition, TransactionPayload};
use stacks::chainstate::stacks::db::ClarityTx;
use super::RunLoopCallbacks;
/// RunLoop is coordinating a simulated burnchain and some simulated nodes
/// taking turns in producing blocks.
pub struct RunLoop {
config: Config,
pub node: Node,
burnchain_initialized_callback: Option<fn(&mut Box<dyn BurnchainController>)>,
new_burnchain_state_callback: Option<fn(u64, &BurnchainTip, &ChainTip)>,
new_tenure_callback: Option<fn(u64, &Tenure)>,
new_chain_state_callback: Option<fn(u64, &mut StacksChainState, &ChainTip, &BurnchainTip)>,
}
macro_rules! info_blue {
($($arg:tt)*) => ({
eprintln!("\x1b[0;96m{}\x1b[0m", format!($($arg)*));
})
}
#[allow(unused_macros)]
macro_rules! info_yellow {
($($arg:tt)*) => ({
eprintln!("\x1b[0;33m{}\x1b[0m", format!($($arg)*));
})
}
macro_rules! info_green {
($($arg:tt)*) => ({
eprintln!("\x1b[0;32m{}\x1b[0m", format!($($arg)*));
})
pub callbacks: RunLoopCallbacks,
}
impl RunLoop {
@@ -48,10 +27,7 @@ impl RunLoop {
Self {
config,
node,
burnchain_initialized_callback: None,
new_burnchain_state_callback: None,
new_tenure_callback: None,
new_chain_state_callback: None,
callbacks: RunLoopCallbacks::new(),
}
}
@@ -65,7 +41,7 @@ impl RunLoop {
// Initialize and start the burnchain.
let mut burnchain: Box<dyn BurnchainController> = match &self.config.burnchain.mode[..] {
"helium" | "neon" => {
"helium" => {
BitcoinRegtestController::generic(self.config.clone())
},
"mocknet" => {
@@ -74,14 +50,12 @@ impl RunLoop {
_ => unreachable!()
};
RunLoop::handle_burnchain_initialized_cb(
&self.burnchain_initialized_callback,
&mut burnchain);
self.callbacks.invoke_burn_chain_initialized(&mut burnchain);
let genesis_state = burnchain.start();
let initial_state = burnchain.start();
// Update each node with the genesis block.
self.node.process_burnchain_state(&genesis_state);
self.node.process_burnchain_state(&initial_state);
// make first non-genesis block, with initial VRF keys
self.node.setup(&mut burnchain);
@@ -92,7 +66,8 @@ impl RunLoop {
// Sync and update node with this new block.
let burnchain_tip = burnchain.sync();
self.node.process_burnchain_state(&burnchain_tip);
self.node.process_burnchain_state(&burnchain_tip); // todo(ludo): should return genesis?
let mut chain_tip = ChainTip::genesis();
if self.config.burnchain.mode == "mocknet" {
self.node.spawn_peer_server();
@@ -106,7 +81,7 @@ impl RunLoop {
None => panic!("Error while initiating genesis tenure")
};
RunLoop::handle_new_tenure_cb(&self.new_tenure_callback, round_index, &first_tenure);
self.callbacks.invoke_new_tenure(round_index, &burnchain_tip, &chain_tip, &first_tenure);
// Run the tenure, keep the artifacts
let artifacts_from_1st_tenure = match first_tenure.run() {
@@ -125,38 +100,30 @@ impl RunLoop {
artifacts_from_1st_tenure.burn_fee);
let mut burnchain_tip = burnchain.sync();
RunLoop::handle_burnchain_state_cb(
&self.new_burnchain_state_callback,
round_index,
&burnchain_tip,
&ChainTip::genesis());
self.callbacks.invoke_new_burn_chain_state(round_index, &burnchain_tip, &chain_tip);
let mut leader_tenure = None;
// Have each node process the new block, that should include a sortition thanks to the
// 1st tenure.
let (last_sortitioned_block, won_sortition) = match self.node.process_burnchain_state(&burnchain_tip) {
(Some(sortitioned_block), won_sortition) => (sortitioned_block, won_sortition),
(None, _) => panic!("Node should have a sortitioned block")
};
// Have each node process the previous tenure.
// Have the node process its own tenure.
// We should have some additional checks here, and ensure that the previous artifacts are legit.
let mut chain_tip = self.node.process_tenure(
chain_tip = self.node.process_tenure(
&artifacts_from_1st_tenure.anchored_block,
&last_sortitioned_block.block_snapshot.burn_header_hash,
&last_sortitioned_block.block_snapshot.parent_burn_header_hash,
artifacts_from_1st_tenure.microblocks.clone(),
burnchain.burndb_mut());
RunLoop::handle_new_chain_state_cb(
&self.new_chain_state_callback,
self.callbacks.invoke_new_stacks_chain_state(
round_index,
&mut self.node.chain_state,
&chain_tip,
&burnchain_tip);
&burnchain_tip,
&chain_tip,
&mut self.node.chain_state);
// If the node we're looping on won the sortition, initialize and configure the next tenure
if won_sortition {
@@ -173,7 +140,7 @@ impl RunLoop {
// Run the last initialized tenure
let artifacts_from_tenure = match leader_tenure {
Some(mut tenure) => {
RunLoop::handle_new_tenure_cb(&self.new_tenure_callback, round_index, &tenure);
self.callbacks.invoke_new_tenure(round_index, &burnchain_tip, &chain_tip, &tenure);
tenure.run()
},
None => None
@@ -192,11 +159,7 @@ impl RunLoop {
}
burnchain_tip = burnchain.sync();
RunLoop::handle_burnchain_state_cb(
&self.new_burnchain_state_callback,
round_index,
&burnchain_tip,
&chain_tip);
self.callbacks.invoke_new_burn_chain_state(round_index, &burnchain_tip, &chain_tip);
leader_tenure = None;
@@ -210,7 +173,7 @@ impl RunLoop {
// Pass if we're missing the artifacts from the current tenure.
None => continue,
Some(ref artifacts) => {
// Have each node process the previous tenure.
// Have the node process its tenure.
// We should have some additional checks here, and ensure that the previous artifacts are legit.
chain_tip = self.node.process_tenure(
&artifacts.anchored_block,
@@ -219,17 +182,15 @@ impl RunLoop {
artifacts.microblocks.clone(),
burnchain.burndb_mut());
RunLoop::handle_new_chain_state_cb(
&self.new_chain_state_callback,
round_index,
&mut self.node.chain_state,
&chain_tip,
&burnchain_tip
);
self.callbacks.invoke_new_stacks_chain_state(
round_index,
&burnchain_tip,
&chain_tip,
&mut self.node.chain_state);
},
};
// If the node we're looping on won the sortition, initialize and configure the next tenure
// If won sortition, initialize and configure the next tenure
if won_sortition {
leader_tenure = self.node.initiate_new_tenure();
}
@@ -237,60 +198,4 @@ impl RunLoop {
round_index += 1;
}
}
pub fn apply_once_burnchain_initialized(&mut self, f: fn(&mut Box<dyn BurnchainController>)) {
self.burnchain_initialized_callback = Some(f);
}
pub fn apply_on_new_burnchain_states(&mut self, f: fn(u64, &BurnchainTip, &ChainTip)) {
self.new_burnchain_state_callback = Some(f);
}
pub fn apply_on_new_tenures(&mut self, f: fn(u64, &Tenure)) {
self.new_tenure_callback = Some(f);
}
pub fn apply_on_new_chain_states(&mut self, f: fn(u64, &mut StacksChainState, &ChainTip, &BurnchainTip)) {
self.new_chain_state_callback = Some(f);
}
fn handle_burnchain_initialized_cb(burnchain_initialized_callback: &Option<fn(&mut Box<dyn BurnchainController>)>, burnchain_controller: &mut Box<dyn BurnchainController>) {
burnchain_initialized_callback.map(|cb| cb(burnchain_controller));
}
fn handle_new_tenure_cb(new_tenure_callback: &Option<fn(u64, &Tenure)>,
round_index: u64, tenure: &Tenure) {
new_tenure_callback.map(|cb| cb(round_index, tenure));
}
fn handle_burnchain_state_cb(burn_callback: &Option<fn(u64, &BurnchainTip, & ChainTip)>,
round_index: u64, burnchain_tip: &BurnchainTip, chain_tip: &ChainTip) {
info_blue!("Burnchain block #{} ({}) was produced with sortition #{}",
burnchain_tip.block_snapshot.block_height,
burnchain_tip.block_snapshot.burn_header_hash,
burnchain_tip.block_snapshot.sortition_hash);
burn_callback.map(|cb| cb(round_index, burnchain_tip, chain_tip));
}
fn handle_new_chain_state_cb(chain_state_callback: &Option<fn(u64, &mut StacksChainState, &ChainTip, &BurnchainTip)>,
round_index: u64, state: &mut StacksChainState, chain_tip: &ChainTip, burnchain_tip: &BurnchainTip) {
info_green!("Stacks block #{} ({}) successfully produced, including {} transactions",
chain_tip.metadata.block_height,
chain_tip.metadata.index_block_hash(),
chain_tip.block.txs.len());
for tx in chain_tip.block.txs.iter() {
match &tx.auth {
TransactionAuth::Standard(TransactionSpendingCondition::Singlesig(auth)) => println!("-> Tx issued by {:?} (fee: {}, nonce: {})", auth.signer, auth.fee_rate, auth.nonce),
_ => println!("-> Tx {:?}", tx.auth)
}
match &tx.payload {
TransactionPayload::Coinbase(_) => println!(" Coinbase"),
TransactionPayload::SmartContract(contract) => println!(" Publish smart contract\n**************************\n{:?}\n**************************", contract.code_body),
TransactionPayload::TokenTransfer(recipent, amount, _) => println!(" Transfering {} µSTX to {}", amount, recipent.to_string()),
_ => println!(" {:?}", tx.payload)
}
}
chain_state_callback.map(|cb| cb(round_index, state, chain_tip, burnchain_tip));
}
}

107
testnet/src/run_loop/mod.rs Normal file
View File

@@ -0,0 +1,107 @@
pub mod helium;
pub mod neon;
use crate::{BurnchainController, BurnchainTip, ChainTip, Tenure};
use stacks::chainstate::stacks::{TransactionAuth, TransactionSpendingCondition, TransactionPayload};
use stacks::chainstate::stacks::db::StacksChainState;
macro_rules! info_blue {
($($arg:tt)*) => ({
eprintln!("\x1b[0;96m{}\x1b[0m", format!($($arg)*));
})
}
#[allow(unused_macros)]
macro_rules! info_yellow {
($($arg:tt)*) => ({
eprintln!("\x1b[0;33m{}\x1b[0m", format!($($arg)*));
})
}
macro_rules! info_green {
($($arg:tt)*) => ({
eprintln!("\x1b[0;32m{}\x1b[0m", format!($($arg)*));
})
}
pub struct RunLoopCallbacks {
on_burn_chain_initialized: Option<fn(&mut Box<dyn BurnchainController>)>,
on_new_burn_chain_state: Option<fn(u64, &BurnchainTip, &ChainTip)>,
on_new_stacks_chain_state: Option<fn(u64, &BurnchainTip, &ChainTip, &mut StacksChainState)>,
on_new_tenure: Option<fn(u64, &BurnchainTip, &ChainTip, &Tenure)>,
}
impl RunLoopCallbacks {
pub fn new() -> RunLoopCallbacks {
RunLoopCallbacks {
on_burn_chain_initialized: None,
on_new_burn_chain_state: None,
on_new_stacks_chain_state: None,
on_new_tenure: None,
}
}
pub fn on_burn_chain_initialized(&mut self, callback: fn(&mut Box<dyn BurnchainController>)) {
self.on_burn_chain_initialized = Some(callback);
}
pub fn on_new_burn_chain_state(&mut self, callback: fn(u64, &BurnchainTip, &ChainTip)) {
self.on_new_burn_chain_state = Some(callback);
}
pub fn on_new_stacks_chain_state(&mut self, callback: fn(u64, &BurnchainTip, &ChainTip, &mut StacksChainState)) {
self.on_new_stacks_chain_state = Some(callback);
}
pub fn on_new_tenure(&mut self, callback: fn(u64, &BurnchainTip, &ChainTip, &Tenure)) {
self.on_new_tenure = Some(callback);
}
pub fn invoke_burn_chain_initialized(&self, burnchain: &mut Box<dyn BurnchainController>) {
if let Some(cb) = self.on_burn_chain_initialized {
cb(burnchain);
}
}
pub fn invoke_new_burn_chain_state(&self, round: u64, burnchain_tip: &BurnchainTip, chain_tip: &ChainTip) {
info_blue!("Burnchain block #{} ({}) was produced with sortition #{}",
burnchain_tip.block_snapshot.block_height,
burnchain_tip.block_snapshot.burn_header_hash,
burnchain_tip.block_snapshot.sortition_hash);
if let Some(cb) = self.on_new_burn_chain_state {
cb(round, burnchain_tip, chain_tip);
}
}
pub fn invoke_new_stacks_chain_state(&self, round: u64, burnchain_tip: &BurnchainTip, chain_tip: &ChainTip, chain_state: &mut StacksChainState) {
info_green!("Stacks block #{} ({}) successfully produced, including {} transactions",
chain_tip.metadata.block_height,
chain_tip.metadata.index_block_hash(),
chain_tip.block.txs.len());
for tx in chain_tip.block.txs.iter() {
match &tx.auth {
TransactionAuth::Standard(TransactionSpendingCondition::Singlesig(auth)) => println!("-> Tx issued by {:?} (fee: {}, nonce: {})", auth.signer, auth.fee_rate, auth.nonce),
_ => println!("-> Tx {:?}", tx.auth)
}
match &tx.payload {
TransactionPayload::Coinbase(_) => println!(" Coinbase"),
TransactionPayload::SmartContract(contract) => println!(" Publish smart contract\n**************************\n{:?}\n**************************", contract.code_body),
TransactionPayload::TokenTransfer(recipent, amount, _) => println!(" Transfering {} µSTX to {}", amount, recipent.to_string()),
_ => println!(" {:?}", tx.payload)
}
}
if let Some(cb) = self.on_new_stacks_chain_state {
cb(round, burnchain_tip, chain_tip, chain_state);
}
}
pub fn invoke_new_tenure(&self, round: u64, burnchain_tip: &BurnchainTip, chain_tip: &ChainTip, tenure: &Tenure) {
if let Some(cb) = self.on_new_tenure {
cb(round, burnchain_tip, chain_tip, tenure);
}
}
}

View File

@@ -0,0 +1,125 @@
use crate::{Config, Node, BurnchainController, BitcoinRegtestController, ChainTip};
use stacks::chainstate::stacks::db::ClarityTx;
use super::RunLoopCallbacks;
/// Coordinating a node running in neon mode.
pub struct RunLoop {
config: Config,
pub node: Node,
pub callbacks: RunLoopCallbacks,
}
impl RunLoop {
pub fn new(config: Config) -> Self {
RunLoop::new_with_boot_exec(config, |_| {})
}
/// Sets up a runloop and node, given a config.
pub fn new_with_boot_exec<F>(config: Config, boot_exec: F) -> Self
where F: Fn(&mut ClarityTx) -> () {
// Build node based on config
let node = Node::new(config.clone(), boot_exec);
Self {
config,
node,
callbacks: RunLoopCallbacks::new()
}
}
/// Starts the testnet runloop.
///
/// This function will block by looping infinitely.
/// It will start the burnchain (separate thread), set-up a channel in
/// charge of coordinating the new blocks coming from the burnchain and
/// the nodes, taking turns on tenures.
pub fn start(&mut self, expected_num_rounds: u64) {
// Initialize and start the burnchain.
let mut burnchain: Box<dyn BurnchainController> = BitcoinRegtestController::generic(self.config.clone());
self.callbacks.invoke_burn_chain_initialized(&mut burnchain);
let mut burnchain_tip = burnchain.start();
// TODO: enable this once available
// self.node.spawn_peer_server();
// todo(ludo): ensure that burnchain_state and chain_state are consistent
// todo(ludo): node should retrieve prior keys, thanks to burnchain
self.node.process_burnchain_state(&burnchain_tip);
self.node.setup(&mut burnchain);
let mut round_index: u64 = 1;
let mut leader_tenure = None;
let mut chain_tip = ChainTip::genesis(); // todo(ludo): fix
// Start the runloop
loop {
if expected_num_rounds == round_index {
return;
}
// Run the last initialized tenure
let artifacts_from_tenure = match leader_tenure {
Some(mut tenure) => {
self.callbacks.invoke_new_tenure(round_index, &burnchain_tip, &chain_tip, &tenure);
tenure.run()
},
None => None
};
match artifacts_from_tenure {
Some(ref artifacts) => {
self.node.commit_artifacts(
&artifacts.anchored_block,
&artifacts.parent_block,
&mut burnchain,
artifacts.burn_fee);
},
None => {}
}
burnchain_tip = burnchain.sync();
self.callbacks.invoke_new_burn_chain_state(round_index, &burnchain_tip, &chain_tip);
leader_tenure = None;
// Have each node process the new block, that can include, or not, a sortition.
let (sortitioned_block, won_sortition) = self.node.process_burnchain_state(&burnchain_tip);
match (artifacts_from_tenure, sortitioned_block) {
// Pass if we're missing the artifacts from the current tenure.
(Some(ref artifacts), Some(ref last_sortitioned_block)) => {
// Have each node process the previous tenure.
// We should have some additional checks here, and ensure that the previous artifacts are legit.
chain_tip = self.node.process_tenure(
&artifacts.anchored_block,
&last_sortitioned_block.block_snapshot.burn_header_hash,
&last_sortitioned_block.block_snapshot.parent_burn_header_hash,
artifacts.microblocks.clone(),
burnchain.burndb_mut());
self.callbacks.invoke_new_stacks_chain_state(
round_index,
&burnchain_tip,
&chain_tip,
&mut self.node.chain_state);
},
(_, _) => continue,
};
// If the node we're looping on won the sortition, initialize and configure the next tenure
if won_sortition {
leader_tenure = self.node.initiate_new_tenure();
}
round_index += 1;
}
}
}

View File

@@ -1,6 +1,7 @@
use std::process::{Command, Stdio, Child};
use super::{Config, RunLoop, MemPool};
use crate::{Config, MemPool};
use crate::helium::RunLoop;
use stacks::chainstate::burn::operations::BlockstackOperationType::{LeaderBlockCommit, LeaderKeyRegister};
use stacks::util::hash::{hex_bytes};
@@ -98,14 +99,14 @@ fn simple_test() {
let num_rounds = 6;
let mut run_loop = RunLoop::new(conf);
run_loop.apply_once_burnchain_initialized(|burnchain_controller| {
run_loop.callbacks.on_burn_chain_initialized(|burnchain_controller| {
// todo(ludo): we need to wait for bitcoind to be ready.
sleep_ms(5000);
burnchain_controller.bootstrap_chain();
});
// In this serie of tests, the callback is fired post-burnchain-sync, pre-stacks-sync
run_loop.apply_on_new_burnchain_states(|round, burnchain_tip, chain_tip| {
run_loop.callbacks.on_new_burn_chain_state(|round, burnchain_tip, chain_tip| {
match round {
0 => {
let block = &burnchain_tip.block_snapshot;
@@ -267,7 +268,7 @@ fn simple_test() {
});
// Use tenure's hook for submitting transactions
run_loop.apply_on_new_tenures(|round, tenure| {
run_loop.callbacks.on_new_tenure(|round, _burnchain_tip, _chain_tip, tenure| {
match round {
1 => {
// On round 1, publish the KV contract
@@ -319,7 +320,7 @@ fn simple_test() {
// Use block's hook for asserting expectations
// In this serie of tests, the callback is fired post-burnchain-sync, post-stacks-sync
run_loop.apply_on_new_chain_states(|round, _chain_state, chain_tip, burnchain_tip| {
run_loop.callbacks.on_new_stacks_chain_state(|round, burnchain_tip, chain_tip, _chain_state| {
match round {
0 => {
// Inspecting the chain at round 0.

View File

@@ -18,7 +18,9 @@ use stacks::net::{StacksMessageCodec, AccountEntryResponse, ContractSrcResponse,
use stacks::util::{strings::StacksString};
use stacks::vm::clarity::ClarityConnection;
use super::super::{MemPool, RunLoop, config::InitialBalance};
use crate::{MemPool, config::InitialBalance};
use crate::helium::RunLoop;
use reqwest;
@@ -193,7 +195,7 @@ fn integration_test_get_info() {
http_opt.replace(format!("http://{}", &run_loop.node.config.node.rpc_bind));
}
run_loop.apply_on_new_tenures(|round, tenure| {
run_loop.callbacks.on_new_tenure(|round, _burnchain_tip, _chain_tip, tenure| {
let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
let principal_sk = StacksPrivateKey::from_hex(SK_2).unwrap();
let spender_sk = StacksPrivateKey::from_hex(SK_3).unwrap();
@@ -216,7 +218,7 @@ fn integration_test_get_info() {
return
});
run_loop.apply_on_new_chain_states(|round, chain_state, chain_tip, _burnchain_tip| {
run_loop.callbacks.on_new_stacks_chain_state(|round, _burnchain_tip, chain_tip, chain_state| {
let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap());
let contract_identifier =
QualifiedContractIdentifier::parse(&format!("{}.{}", &contract_addr, "get-info")).unwrap();
@@ -606,7 +608,7 @@ fn contract_stx_transfer() {
let num_rounds = 5;
let mut run_loop = RunLoop::new(conf);
run_loop.apply_on_new_tenures(|round, tenure| {
run_loop.callbacks.on_new_tenure(|round, _burnchain_tip, _chain_tip, tenure| {
let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
let sk_2 = StacksPrivateKey::from_hex(SK_2).unwrap();
let sk_3 = StacksPrivateKey::from_hex(SK_3).unwrap();
@@ -640,7 +642,7 @@ fn contract_stx_transfer() {
return
});
run_loop.apply_on_new_chain_states(|round, chain_state, chain_tip, _burnchain_tip| {
run_loop.callbacks.on_new_stacks_chain_state(|round, _burnchain_tip, chain_tip, chain_state| {
let contract_identifier =
QualifiedContractIdentifier::parse(&format!("{}.{}",
to_addr(

View File

@@ -16,7 +16,8 @@ use stacks::chainstate::stacks::{
TokenTransferMemo,
StacksTransaction, StacksAddress };
use super::super::{Keychain, MemPool, RunLoop};
use crate::{Keychain, MemPool};
use crate::helium::RunLoop;
const FOO_CONTRACT: &'static str = "(define-public (foo) (ok 1))
(define-public (bar (x uint)) (ok x))";
@@ -58,7 +59,7 @@ fn mempool_setup_chainstate() {
let mut run_loop = RunLoop::new(conf);
run_loop.apply_on_new_tenures(|round, tenure| {
run_loop.callbacks.on_new_tenure(|round, _burnchain_tip, _chain_tip, tenure| {
let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
if round == 0 { // block-height = 2
let publish_tx = make_contract_publish(&contract_sk, 0, 100, "foo_contract", FOO_CONTRACT);
@@ -67,8 +68,8 @@ fn mempool_setup_chainstate() {
}
});
run_loop.apply_on_new_chain_states(|round, chainstate, chain_tip, _burnchain_tip| {
// run_loop.apply_on_new_chain_states(|round, ref mut chainstate, bhh| {
run_loop.callbacks.on_new_stacks_chain_state(|round, _burnchain_tip, chain_tip, chainstate| {
// run_loop.callbacks.on_new_stacks_chain_state(|round, ref mut chainstate, bhh| {
let contract_sk = StacksPrivateKey::from_hex(SK_1).unwrap();
let contract_addr = to_addr(&contract_sk);

View File

@@ -6,7 +6,8 @@ use stacks::chainstate::stacks::events::{StacksTransactionEvent, STXEventType};
use stacks::chainstate::stacks::{TransactionPayload};
use stacks::util::hash::{hex_bytes};
use super::{MemPool, Config, RunLoop};
use super::{MemPool, Config};
use crate::helium::RunLoop;
use super::node::{TESTNET_CHAIN_ID};
pub fn new_test_conf() -> Config {
@@ -28,7 +29,7 @@ fn should_succeed_mining_valid_txs() {
let mut run_loop = RunLoop::new(conf);
// Use tenure's hook for submitting transactions
run_loop.apply_on_new_tenures(|round, tenure| {
run_loop.callbacks.on_new_tenure(|round, _burnchain_tip, _chain_tip, tenure| {
match round {
1 => {
// On round 1, publish the KV contract
@@ -79,7 +80,7 @@ fn should_succeed_mining_valid_txs() {
});
// Use block's hook for asserting expectations
run_loop.apply_on_new_chain_states(|round, _chain_state, chain_tip, _burnchain_tip| {
run_loop.callbacks.on_new_stacks_chain_state(|round, _burnchain_tip, chain_tip, _chain_state| {
match round {
0 => {
// Inspecting the chain at round 0.
@@ -272,7 +273,7 @@ fn should_succeed_handling_malformed_and_valid_txs() {
let mut run_loop = RunLoop::new(conf);
// Use tenure's hook for submitting transactions
run_loop.apply_on_new_tenures(|round, tenure| {
run_loop.callbacks.on_new_tenure(|round, _burnchain_tip, _chain_tip, tenure| {
match round {
1 => {
// On round 1, publish the KV contract
@@ -319,7 +320,7 @@ fn should_succeed_handling_malformed_and_valid_txs() {
});
// Use block's hook for asserting expectations
run_loop.apply_on_new_chain_states(|round, _chain_state, chain_tip, _burnchain_tip| {
run_loop.callbacks.on_new_stacks_chain_state(|round, _burnchain_tip, chain_tip, _chain_state| {
match round {
0 => {
// Inspecting the chain at round 0.