Skip to content

Commit f42bc48

Browse files
committed
Fix miner forking by being strict about sortition winners
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 8c9d129 commit f42bc48

File tree

1 file changed

+232
-125
lines changed
  • testnet/stacks-node/src/tests/signer

1 file changed

+232
-125
lines changed

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 232 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1806,8 +1806,8 @@ fn miner_forking() {
18061806

18071807
let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap();
18081808
let Counters {
1809-
naka_skip_commit_op,
1810-
naka_submitted_commits: second_miner_commits_submitted,
1809+
naka_skip_commit_op: skip_commit_op_rl2,
1810+
naka_submitted_commits: commits_submitted_rl2,
18111811
..
18121812
} = run_loop_2.counters();
18131813
let _run_loop_2_thread = thread::Builder::new()
@@ -1828,149 +1828,256 @@ fn miner_forking() {
18281828
})
18291829
.expect("Timed out waiting for boostrapped node to catch up to the miner");
18301830

1831+
let commits_submitted_rl1 = signer_test.running_nodes.commits_submitted.clone();
1832+
let skip_commit_op_rl1 = signer_test
1833+
.running_nodes
1834+
.nakamoto_test_skip_commit_op
1835+
.clone();
1836+
18311837
let pre_nakamoto_peer_1_height = get_chain_info(&conf).stacks_tip_height;
18321838

1833-
naka_skip_commit_op.set(true);
1839+
let mining_pk_1 = StacksPublicKey::from_private(&conf.miner.mining_key.unwrap());
1840+
let mining_pk_2 = StacksPublicKey::from_private(&conf_node_2.miner.mining_key.unwrap());
1841+
let mining_pkh_1 = Hash160::from_node_public_key(&mining_pk_1);
1842+
let mining_pkh_2 = Hash160::from_node_public_key(&mining_pk_2);
1843+
debug!("The mining key for miner 1 is {mining_pkh_1}");
1844+
debug!("The mining key for miner 2 is {mining_pkh_2}");
1845+
1846+
let sortdb = conf.get_burnchain().open_sortition_db(true).unwrap();
1847+
let get_burn_height = || {
1848+
SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
1849+
.unwrap()
1850+
.block_height
1851+
};
18341852
info!("------------------------- Reached Epoch 3.0 -------------------------");
18351853

1836-
let mut sortitions_seen = Vec::new();
1837-
let run_sortition = || {
1838-
info!("Pausing stacks block proposal to force an empty tenure commit from RL2");
1839-
TEST_BROADCAST_STALL.lock().unwrap().replace(true);
1854+
info!("Pausing both miners' block commit submissions");
1855+
skip_commit_op_rl1.set(true);
1856+
skip_commit_op_rl2.set(true);
18401857

1841-
let rl2_commits_before = second_miner_commits_submitted.load(Ordering::SeqCst);
1842-
let rl1_commits_before = signer_test
1843-
.running_nodes
1844-
.commits_submitted
1845-
.load(Ordering::SeqCst);
1858+
info!("Flushing any pending commits to enable custom winner selection");
1859+
let burn_height_before = get_burn_height();
1860+
next_block_and(
1861+
&mut signer_test.running_nodes.btc_regtest_controller,
1862+
30,
1863+
|| Ok(get_burn_height() > burn_height_before),
1864+
)
1865+
.unwrap();
18461866

1847-
signer_test
1848-
.running_nodes
1849-
.btc_regtest_controller
1850-
.build_next_block(1);
1851-
naka_skip_commit_op.set(false);
1867+
info!("------------------------- RL1 Wins Sortition -------------------------");
1868+
info!("Pausing stacks block proposal to force an empty tenure commit from RL2");
1869+
TEST_BROADCAST_STALL.lock().unwrap().replace(true);
1870+
let rl1_commits_before = commits_submitted_rl1.load(Ordering::SeqCst);
18521871

1853-
// wait until a commit is submitted by run_loop_2
1854-
wait_for(60, || {
1855-
let commits_count = second_miner_commits_submitted.load(Ordering::SeqCst);
1856-
Ok(commits_count > rl2_commits_before)
1857-
})
1872+
info!("Unpausing commits from RL1");
1873+
skip_commit_op_rl1.set(false);
1874+
1875+
info!("Waiting for commits from RL1");
1876+
wait_for(30, || {
1877+
Ok(commits_submitted_rl1.load(Ordering::SeqCst) > rl1_commits_before)
1878+
})
1879+
.expect("Timed out waiting for miner 1 to submit a commit op");
1880+
1881+
info!("Pausing commits from RL1");
1882+
skip_commit_op_rl1.set(true);
1883+
1884+
let burn_height_before = get_burn_height();
1885+
info!("Mine RL1 Tenure");
1886+
next_block_and(
1887+
&mut signer_test.running_nodes.btc_regtest_controller,
1888+
30,
1889+
|| Ok(get_burn_height() > burn_height_before),
1890+
)
1891+
.unwrap();
1892+
1893+
// fetch the current sortition info
1894+
let sortdb = conf.get_burnchain().open_sortition_db(true).unwrap();
1895+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
1896+
// make sure the tenure was won by RL1
1897+
assert!(tip.sortition, "No sortition was won");
1898+
assert_eq!(
1899+
tip.miner_pk_hash.unwrap(),
1900+
mining_pkh_1,
1901+
"RL1 did not win the sortition"
1902+
);
1903+
1904+
info!(
1905+
"------------------------- RL2 Wins Sortition With Outdated View -------------------------"
1906+
);
1907+
let rl2_commits_before = commits_submitted_rl2.load(Ordering::SeqCst);
1908+
1909+
info!("Unpausing commits from RL2");
1910+
skip_commit_op_rl2.set(false);
1911+
1912+
info!("Waiting for commits from RL2");
1913+
wait_for(30, || {
1914+
Ok(commits_submitted_rl2.load(Ordering::SeqCst) > rl2_commits_before)
1915+
})
1916+
.expect("Timed out waiting for miner 1 to submit a commit op");
1917+
1918+
info!("Pausing commits from RL2");
1919+
skip_commit_op_rl2.set(true);
1920+
1921+
// unblock block mining
1922+
let blocks_len = test_observer::get_blocks().len();
1923+
TEST_BROADCAST_STALL.lock().unwrap().replace(false);
1924+
1925+
// Wait for the block to be broadcasted and processed
1926+
wait_for(30, || Ok(test_observer::get_blocks().len() > blocks_len))
1927+
.expect("Timed out waiting for a block to be processed");
1928+
1929+
// sleep for 2*first_proposal_burn_block_timing to prevent the block timing from allowing a fork by the signer set
1930+
thread::sleep(Duration::from_secs(first_proposal_burn_block_timing * 2));
1931+
1932+
let nakamoto_headers: HashMap<_, _> = get_nakamoto_headers(&conf)
1933+
.into_iter()
1934+
.map(|header| {
1935+
info!("Nakamoto block"; "height" => header.stacks_block_height, "consensus_hash" => %header.consensus_hash, "last_sortition_hash" => %tip.consensus_hash);
1936+
(header.consensus_hash, header)
1937+
})
1938+
.collect();
1939+
1940+
let header_info = nakamoto_headers.get(&tip.consensus_hash).unwrap();
1941+
let header = header_info
1942+
.anchored_header
1943+
.as_stacks_nakamoto()
1944+
.unwrap()
1945+
.clone();
1946+
1947+
mining_pk_1
1948+
.verify(
1949+
header.miner_signature_hash().as_bytes(),
1950+
&header.miner_signature,
1951+
)
18581952
.unwrap();
1859-
// wait until a commit is submitted by run_loop_1
1860-
wait_for(60, || {
1861-
let commits_count = signer_test
1862-
.running_nodes
1863-
.commits_submitted
1864-
.load(Ordering::SeqCst);
1865-
Ok(commits_count > rl1_commits_before)
1953+
1954+
let blocks_len = test_observer::get_blocks().len();
1955+
let burn_height_before = get_burn_height();
1956+
info!("Mine RL2 Tenure");
1957+
next_block_and(
1958+
&mut signer_test.running_nodes.btc_regtest_controller,
1959+
30,
1960+
|| Ok(get_burn_height() > burn_height_before),
1961+
)
1962+
.unwrap();
1963+
1964+
// Ensure that RL2 doesn't produce a valid block
1965+
assert!(
1966+
wait_for(60, || Ok(test_observer::get_blocks().len() > blocks_len)).is_err(),
1967+
"RL2 produced a block"
1968+
);
1969+
1970+
// fetch the current sortition info
1971+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
1972+
// make sure the tenure was won by RL2
1973+
assert!(tip.sortition, "No sortition was won");
1974+
assert_eq!(
1975+
tip.miner_pk_hash.unwrap(),
1976+
mining_pkh_2,
1977+
"RL2 did not win the sortition"
1978+
);
1979+
1980+
let nakamoto_headers: HashMap<_, _> = get_nakamoto_headers(&conf)
1981+
.into_iter()
1982+
.map(|header| {
1983+
info!("Nakamoto block"; "height" => header.stacks_block_height, "consensus_hash" => %header.consensus_hash, "last_sortition_hash" => %tip.consensus_hash);
1984+
(header.consensus_hash, header)
18661985
})
1867-
.unwrap();
1986+
.collect();
1987+
assert!(!nakamoto_headers.contains_key(&tip.consensus_hash));
18681988

1869-
// fetch the current sortition info
1870-
let sortdb = conf.get_burnchain().open_sortition_db(true).unwrap();
1871-
let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
1989+
info!("------------------------- RL1 RBFs its Own Commit -------------------------");
1990+
info!("Pausing stacks block proposal to test RBF capability");
1991+
TEST_BROADCAST_STALL.lock().unwrap().replace(true);
1992+
let rl1_commits_before = commits_submitted_rl1.load(Ordering::SeqCst);
18721993

1873-
// block commits from RL2 -- this will block until the start of the next iteration
1874-
// in this loop.
1875-
naka_skip_commit_op.set(true);
1876-
// ensure RL1 performs an RBF after unblock block broadcast
1877-
let rl1_commits_before = signer_test
1878-
.running_nodes
1879-
.commits_submitted
1880-
.load(Ordering::SeqCst);
1994+
info!("Unpausing commits from RL1");
1995+
skip_commit_op_rl1.set(false);
18811996

1882-
// unblock block mining
1883-
let blocks_len = test_observer::get_blocks().len();
1884-
TEST_BROADCAST_STALL.lock().unwrap().replace(false);
1997+
info!("Waiting for commits from RL1");
1998+
wait_for(30, || {
1999+
Ok(commits_submitted_rl1.load(Ordering::SeqCst) > rl1_commits_before)
2000+
})
2001+
.expect("Timed out waiting for miner 1 to submit a commit op");
18852002

1886-
// wait for a block to be processed (or timeout!)
1887-
if wait_for(60, || Ok(test_observer::get_blocks().len() > blocks_len)).is_err() {
1888-
info!("Timeout waiting for a block process: assuming this is because RL2 attempted to fork-- will check at end of test");
1889-
return (sort_tip, false);
1890-
}
2003+
info!("Pausing commits from RL1");
2004+
skip_commit_op_rl1.set(true);
18912005

1892-
info!("Nakamoto block processed, waiting for commit from RL1");
2006+
let burn_height_before = get_burn_height();
2007+
info!("Mine RL1 Tenure");
2008+
next_block_and(
2009+
&mut signer_test.running_nodes.btc_regtest_controller,
2010+
30,
2011+
|| Ok(get_burn_height() > burn_height_before),
2012+
)
2013+
.unwrap();
18932014

1894-
// wait for a commit from RL1
1895-
wait_for(60, || {
1896-
let commits_count = signer_test
1897-
.running_nodes
1898-
.commits_submitted
1899-
.load(Ordering::SeqCst);
1900-
Ok(commits_count > rl1_commits_before)
1901-
})
1902-
.unwrap();
2015+
let rl1_commits_before = commits_submitted_rl1.load(Ordering::SeqCst);
19032016

1904-
// sleep for 2*first_proposal_burn_block_timing to prevent the block timing from allowing a fork by the signer set
1905-
thread::sleep(Duration::from_secs(first_proposal_burn_block_timing * 2));
1906-
(sort_tip, true)
1907-
};
2017+
info!("Unpausing commits from RL1");
2018+
skip_commit_op_rl1.set(false);
19082019

1909-
let mut won_by_miner_2_but_no_tenure = false;
1910-
let mut won_by_miner_1_after_tenureless_miner_2 = false;
1911-
let miner_1_pk = StacksPublicKey::from_private(conf.miner.mining_key.as_ref().unwrap());
1912-
// miner 2 is expected to be valid iff:
1913-
// (a) its the first nakamoto tenure
1914-
// (b) the prior sortition didn't have a tenure (because by this time RL2 will have up-to-date block processing)
1915-
let mut expects_miner_2_to_be_valid = true;
1916-
// due to the random nature of mining sortitions, the way this test is structured
1917-
// is that keeps track of two scenarios that we want to cover, and once enough sortitions
1918-
// have been produced to cover those scenarios, it stops and checks the results at the end.
1919-
while !(won_by_miner_2_but_no_tenure && won_by_miner_1_after_tenureless_miner_2) {
1920-
let nmb_sortitions_seen = sortitions_seen.len();
1921-
assert!(max_sortitions >= nmb_sortitions_seen, "Produced {nmb_sortitions_seen} sortitions, but didn't cover the test scenarios, aborting");
1922-
let (sortition_data, had_tenure) = run_sortition();
1923-
sortitions_seen.push((sortition_data.clone(), had_tenure));
1924-
1925-
let nakamoto_headers: HashMap<_, _> = get_nakamoto_headers(&conf)
1926-
.into_iter()
1927-
.map(|header| {
1928-
info!("Nakamoto block"; "height" => header.stacks_block_height, "consensus_hash" => %header.consensus_hash, "last_sortition_hash" => %sortition_data.consensus_hash);
1929-
(header.consensus_hash, header)
1930-
})
1931-
.collect();
2020+
info!("Waiting for commits from RL1");
2021+
wait_for(30, || {
2022+
Ok(commits_submitted_rl1.load(Ordering::SeqCst) > rl1_commits_before)
2023+
})
2024+
.expect("Timed out waiting for miner 1 to submit a commit op");
19322025

1933-
if had_tenure {
1934-
let header_info = nakamoto_headers
1935-
.get(&sortition_data.consensus_hash)
1936-
.unwrap();
1937-
let header = header_info
1938-
.anchored_header
1939-
.as_stacks_nakamoto()
1940-
.unwrap()
1941-
.clone();
1942-
let mined_by_miner_1 = miner_1_pk
1943-
.verify(
1944-
header.miner_signature_hash().as_bytes(),
1945-
&header.miner_signature,
1946-
)
1947-
.unwrap();
2026+
let rl1_commits_before = commits_submitted_rl1.load(Ordering::SeqCst);
2027+
// unblock block mining
2028+
let blocks_len = test_observer::get_blocks().len();
2029+
TEST_BROADCAST_STALL.lock().unwrap().replace(false);
19482030

1949-
info!("Block check";
1950-
"height" => header.chain_length,
1951-
"consensus_hash" => %header.consensus_hash,
1952-
"block_hash" => %header.block_hash(),
1953-
"stacks_block_id" => %header.block_id(),
1954-
"mined_by_miner_1?" => mined_by_miner_1,
1955-
"expects_miner_2_to_be_valid?" => expects_miner_2_to_be_valid);
1956-
if !mined_by_miner_1 {
1957-
assert!(expects_miner_2_to_be_valid, "If a block was produced by miner 2, we should have expected miner 2 to be valid");
1958-
} else if won_by_miner_2_but_no_tenure {
1959-
// the tenure was won by miner 1, they produced a block, and this follows a tenure that miner 2 won but couldn't
1960-
// mine during because they tried to fork.
1961-
won_by_miner_1_after_tenureless_miner_2 = true;
1962-
}
2031+
// Wait for the block to be broadcasted and processed
2032+
wait_for(30, || Ok(test_observer::get_blocks().len() > blocks_len))
2033+
.expect("Timed out waiting for a block to be processed");
19632034

1964-
// even if it was mined by miner 2, their next block commit should be invalid!
1965-
expects_miner_2_to_be_valid = false;
1966-
} else {
1967-
info!("Sortition without tenure"; "expects_miner_2_to_be_valid?" => expects_miner_2_to_be_valid);
1968-
assert!(!nakamoto_headers.contains_key(&sortition_data.consensus_hash));
1969-
assert!(!expects_miner_2_to_be_valid, "If no blocks were produced in the tenure, it should be because miner 2 committed to a fork");
1970-
won_by_miner_2_but_no_tenure = true;
1971-
expects_miner_2_to_be_valid = true;
1972-
}
1973-
}
2035+
info!("Ensure that RL1 performs an RBF after unblocking block broadcast");
2036+
wait_for(30, || {
2037+
Ok(commits_submitted_rl1.load(Ordering::SeqCst) > rl1_commits_before)
2038+
})
2039+
.expect("Timed out waiting for miner 1 to RBF its old commit op");
2040+
2041+
info!("Mine RL1 Tenure");
2042+
signer_test
2043+
.running_nodes
2044+
.btc_regtest_controller
2045+
.build_next_block(1);
2046+
2047+
// fetch the current sortition info
2048+
let sortdb = conf.get_burnchain().open_sortition_db(true).unwrap();
2049+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
2050+
// make sure the tenure was won by RL1
2051+
assert!(tip.sortition, "No sortition was won");
2052+
assert_eq!(
2053+
tip.miner_pk_hash.unwrap(),
2054+
mining_pkh_1,
2055+
"RL1 did not win the sortition"
2056+
);
2057+
2058+
let nakamoto_headers: HashMap<_, _> = get_nakamoto_headers(&conf)
2059+
.into_iter()
2060+
.map(|header| {
2061+
info!("Nakamoto block"; "height" => header.stacks_block_height, "consensus_hash" => %header.consensus_hash, "last_sortition_hash" => %tip.consensus_hash);
2062+
(header.consensus_hash, header)
2063+
})
2064+
.collect();
2065+
2066+
let header_info = nakamoto_headers.get(&tip.consensus_hash).unwrap();
2067+
let header = header_info
2068+
.anchored_header
2069+
.as_stacks_nakamoto()
2070+
.unwrap()
2071+
.clone();
2072+
2073+
mining_pk_1
2074+
.verify(
2075+
header.miner_signature_hash().as_bytes(),
2076+
&header.miner_signature,
2077+
)
2078+
.unwrap();
2079+
2080+
info!("------------------------- Verify Peer Data -------------------------");
19742081

19752082
let peer_1_height = get_chain_info(&conf).stacks_tip_height;
19762083
let peer_2_height = get_chain_info(&conf_node_2).stacks_tip_height;

0 commit comments

Comments
 (0)