Skip to content

Commit dd4929b

Browse files
committed
Merge branch 'master' into chore/merge-2.05.0.2.0-to-next
2 parents cef2f5d + 26bfd5f commit dd4929b

File tree

6 files changed

+177
-54
lines changed

6 files changed

+177
-54
lines changed

CHANGELOG.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,23 @@ This release will contain consensus-breaking changes.
1515

1616
## [2.05.0.2.0]
1717

18+
### IMPORTANT! READ THIS FIRST
19+
20+
Please read the following **WARNINGs** in their entirety before upgrading.
21+
1822
WARNING: Please be aware that using this node on chainstate prior to this release will cause
19-
the node to spend up to 30 minutes migrating the data to a new schema.
23+
the node to spend **up to 30 minutes** migrating the data to a new schema.
24+
Depending on the storage medium, this may take even longer.
25+
26+
WARNING: This migration process cannot be interrupted. If it is, the chainstate
27+
will be **irrecovarably corrupted and require a sync from genesis.**
28+
29+
WARNING: You will need **at least 2x the disk space** for the migration to work.
30+
This is because a copy of the chainstate will be made in the same directory in
31+
order to apply the new schema.
32+
33+
It is highly recommended that you **back up your chainstate** before running
34+
this version of the software on it.
2035

2136
### Changed
2237
- The MARF implementation will now defer calculating the root hash of a new trie
@@ -30,9 +45,7 @@ the node to spend up to 30 minutes migrating the data to a new schema.
3045
- The MARF implementation may now cache trie nodes in RAM if directed to do so
3146
by an environment variable (#3042).
3247
- Sortition processing performance has been improved by about an order of
33-
magnitude, by avoiding a slew of expensive database reads (#3045). WARNING:
34-
applying this change to an existing chainstate directory will take a few
35-
minutes when the node starts up.
48+
magnitude, by avoiding a slew of expensive database reads (#3045).
3649
- Updated chains coordinator so that before a Stacks block or a burn block is processed,
3750
an event is sent through the event dispatcher. This fixes #3015.
3851
- Expose a node's public key and public key hash160 (i.e. what appears in

src/chainstate/stacks/index/file.rs

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ use crate::chainstate::stacks::index::TrieLeaf;
5454
use crate::chainstate::stacks::index::{trie_sql, ClarityMarfTrieId, MarfTrieId};
5555

5656
use crate::util_lib::db::sql_pragma;
57+
use crate::util_lib::db::sql_vacuum;
5758
use crate::util_lib::db::sqlite_open;
5859
use crate::util_lib::db::tx_begin_immediate;
5960
use crate::util_lib::db::tx_busy_handler;
@@ -191,14 +192,92 @@ impl TrieFile {
191192
Ok(buf)
192193
}
193194

194-
/// Copy the trie blobs out of a sqlite3 DB into their own file
195-
pub fn export_trie_blobs<T: MarfTrieId>(&mut self, db: &Connection) -> Result<(), Error> {
195+
/// Vacuum the database and report the size before and after.
196+
///
197+
/// Returns database errors. Filesystem errors from reporting the file size change are masked.
198+
fn inner_post_migrate_vacuum(db: &Connection, db_path: &str) -> Result<(), Error> {
199+
// for fun, report the shrinkage
200+
let size_before_opt = fs::metadata(db_path)
201+
.map(|stat| Some(stat.len()))
202+
.unwrap_or(None);
203+
204+
info!("Preemptively vacuuming the database file to free up space after copying trie blobs to a separate file");
205+
sql_vacuum(db)?;
206+
207+
let size_after_opt = fs::metadata(db_path)
208+
.map(|stat| Some(stat.len()))
209+
.unwrap_or(None);
210+
211+
match (size_before_opt, size_after_opt) {
212+
(Some(sz_before), Some(sz_after)) => {
213+
debug!("Shrank DB from {} to {} bytes", sz_before, sz_after);
214+
}
215+
_ => {}
216+
}
217+
218+
Ok(())
219+
}
220+
221+
/// Vacuum the database, and set up and tear down the necessary environment variables to
222+
/// use same parent directory for scratch space.
223+
///
224+
/// Infallible -- any vacuum errors are masked.
225+
fn post_migrate_vacuum(db: &Connection, db_path: &str) {
226+
// set SQLITE_TMPDIR if it isn't set already
227+
let mut set_sqlite_tmpdir = false;
228+
let mut old_tmpdir_opt = None;
229+
if let Some(parent_path) = Path::new(db_path).parent() {
230+
if let Err(_) = env::var("SQLITE_TMPDIR") {
231+
debug!(
232+
"Sqlite will store temporary migration state in '{}'",
233+
parent_path.display()
234+
);
235+
env::set_var("SQLITE_TMPDIR", parent_path);
236+
set_sqlite_tmpdir = true;
237+
}
238+
239+
// also set TMPDIR
240+
old_tmpdir_opt = env::var("TMPDIR").ok();
241+
env::set_var("TMPDIR", parent_path);
242+
}
243+
244+
// don't materialize the error; just warn
245+
let res = TrieFile::inner_post_migrate_vacuum(db, db_path);
246+
if let Err(e) = res {
247+
warn!("Failed to VACUUM the MARF DB post-migration: {:?}", &e);
248+
}
249+
250+
if set_sqlite_tmpdir {
251+
debug!("Unset SQLITE_TMPDIR");
252+
env::remove_var("SQLITE_TMPDIR");
253+
}
254+
if let Some(old_tmpdir) = old_tmpdir_opt {
255+
debug!("Restore TMPDIR to '{}'", &old_tmpdir);
256+
env::set_var("TMPDIR", old_tmpdir);
257+
} else {
258+
debug!("Unset TMPDIR");
259+
env::remove_var("TMPDIR");
260+
}
261+
}
262+
263+
/// Copy the trie blobs out of a sqlite3 DB into their own file.
264+
/// NOTE: this is *not* thread-safe. Do not call while the DB is being used by another thread.
265+
pub fn export_trie_blobs<T: MarfTrieId>(
266+
&mut self,
267+
db: &Connection,
268+
db_path: &str,
269+
) -> Result<(), Error> {
270+
if trie_sql::detect_partial_migration(db)? {
271+
panic!("PARTIAL MIGRATION DETECTED! This is an irrecoverable error. You will need to restart your node from genesis.");
272+
}
273+
196274
let max_block = trie_sql::count_blocks(db)?;
197275
info!(
198276
"Migrate {} blocks to external blob storage at {}",
199277
max_block,
200278
&self.get_path()
201279
);
280+
202281
for block_id in 0..(max_block + 1) {
203282
match trie_sql::is_unconfirmed_block(db, block_id) {
204283
Ok(true) => {
@@ -249,6 +328,11 @@ impl TrieFile {
249328
}
250329
}
251330
}
331+
332+
TrieFile::post_migrate_vacuum(db, db_path);
333+
334+
debug!("Mark MARF trie migration of '{}' as finished", db_path);
335+
trie_sql::set_migrated(db).expect("FATAL: failed to mark DB as migrated");
252336
Ok(())
253337
}
254338
}

src/chainstate/stacks/index/storage.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1439,10 +1439,13 @@ impl<T: MarfTrieId> TrieFileStorage<T> {
14391439
if let Some(blobs) = blobs.as_mut() {
14401440
if TrieFile::exists(&db_path)? {
14411441
// migrate blobs out of the old DB
1442-
blobs.export_trie_blobs::<T>(&db)?;
1442+
blobs.export_trie_blobs::<T>(&db, &db_path)?;
14431443
}
14441444
}
14451445
}
1446+
if trie_sql::detect_partial_migration(&db)? {
1447+
panic!("PARTIAL MIGRATION DETECTED! This is an irrecoverable error. You will need to restart your node from genesis.");
1448+
}
14461449

14471450
debug!(
14481451
"Opened TrieFileStorage {}; external blobs: {}",

src/chainstate/stacks/index/trie_sql.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ use crate::chainstate::stacks::index::node::{
5252
use crate::chainstate::stacks::index::storage::{TrieFileStorage, TrieStorageConnection};
5353
use crate::chainstate::stacks::index::Error;
5454
use crate::chainstate::stacks::index::{trie_sql, BlockMap, MarfTrieId};
55+
use crate::util_lib::db::query_count;
5556
use crate::util_lib::db::query_row;
57+
use crate::util_lib::db::query_rows;
5658
use crate::util_lib::db::sql_pragma;
5759
use crate::util_lib::db::tx_begin_immediate;
5860
use crate::util_lib::db::u64_to_sql;
@@ -96,11 +98,15 @@ static SQL_MARF_DATA_TABLE_SCHEMA_2: &str = "
9698
CREATE TABLE IF NOT EXISTS schema_version (
9799
version INTEGER DEFAULT 1 NOT NULL
98100
);
101+
CREATE TABLE IF NOT EXISTS migrated_version (
102+
version INTEGER DEFAULT 1 NOT NULL
103+
);
99104
ALTER TABLE marf_data ADD COLUMN external_offset INTEGER DEFAULT 0 NOT NULL;
100105
ALTER TABLE marf_data ADD COLUMN external_length INTEGER DEFAULT 0 NOT NULL;
101106
CREATE INDEX IF NOT EXISTS index_external_offset ON marf_data(external_offset);
102107
103108
INSERT OR REPLACE INTO schema_version (version) VALUES (2);
109+
INSERT OR REPLACE INTO migrated_version (version) VALUES (1);
104110
";
105111

106112
pub static SQL_MARF_SCHEMA_VERSION: u64 = 2;
@@ -127,6 +133,19 @@ fn get_schema_version(conn: &Connection) -> u64 {
127133
}
128134
}
129135

136+
/// Get the last schema version before the last attempted migration
137+
fn get_migrated_version(conn: &Connection) -> u64 {
138+
// if the table doesn't exist, then the version is 1.
139+
let sql = "SELECT version FROM migrated_version";
140+
match conn.query_row(sql, NO_PARAMS, |row| row.get::<_, i64>("version")) {
141+
Ok(x) => x as u64,
142+
Err(e) => {
143+
debug!("Failed to get schema version: {:?}", &e);
144+
1u64
145+
}
146+
}
147+
}
148+
130149
/// Migrate the MARF database to the currently-supported schema.
131150
/// Returns the version of the DB prior to the migration.
132151
pub fn migrate_tables_if_needed<T: MarfTrieId>(conn: &mut Connection) -> Result<u64, Error> {
@@ -157,6 +176,14 @@ pub fn migrate_tables_if_needed<T: MarfTrieId>(conn: &mut Connection) -> Result<
157176
}
158177
}
159178
}
179+
if first_version == SQL_MARF_SCHEMA_VERSION
180+
&& get_migrated_version(conn) != SQL_MARF_SCHEMA_VERSION
181+
&& !trie_sql::detect_partial_migration(conn)?
182+
{
183+
// no migration will need to happen, so stop checking
184+
debug!("Marking MARF data as fully-migrated");
185+
set_migrated(conn)?;
186+
}
160187
Ok(first_version)
161188
}
162189

@@ -530,6 +557,39 @@ pub fn get_external_blobs_length(conn: &Connection) -> Result<u64, Error> {
530557
Ok(max_len)
531558
}
532559

560+
/// Do we have a partially-migrated database?
561+
/// Either all tries have offset and length 0, or they all don't. If we have a mixture, then we're
562+
/// corrupted.
563+
pub fn detect_partial_migration(conn: &Connection) -> Result<bool, Error> {
564+
let migrated_version = get_migrated_version(conn);
565+
let schema_version = get_schema_version(conn);
566+
if migrated_version == schema_version {
567+
return Ok(false);
568+
}
569+
570+
let num_migrated = query_count(
571+
conn,
572+
"SELECT COUNT(*) FROM marf_data WHERE external_offset = 0 AND external_length = 0 AND unconfirmed = 0",
573+
NO_PARAMS,
574+
)?;
575+
let num_not_migrated = query_count(
576+
conn,
577+
"SELECT COUNT(*) FROM marf_data WHERE external_offset != 0 AND external_length != 0 AND unconfirmed = 0",
578+
NO_PARAMS,
579+
)?;
580+
Ok(num_migrated > 0 && num_not_migrated > 0)
581+
}
582+
583+
/// Mark a migration as completed
584+
pub fn set_migrated(conn: &Connection) -> Result<(), Error> {
585+
conn.execute(
586+
"UPDATE migrated_version SET version = ?1",
587+
&[&u64_to_sql(SQL_MARF_SCHEMA_VERSION)?],
588+
)
589+
.map_err(|e| e.into())
590+
.and_then(|_| Ok(()))
591+
}
592+
533593
pub fn get_node_hash_bytes(
534594
conn: &Connection,
535595
block_id: u32,

src/util_lib/db.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,13 @@ fn inner_sql_pragma(
523523
conn.pragma_update(None, pragma_name, pragma_value)
524524
}
525525

526+
/// Run a VACUUM command
527+
pub fn sql_vacuum(conn: &Connection) -> Result<(), Error> {
528+
conn.execute("VACUUM", NO_PARAMS)
529+
.map_err(Error::SqliteError)
530+
.and_then(|_| Ok(()))
531+
}
532+
526533
/// Returns true if the database table `table_name` exists in the active
527534
/// database of the provided SQLite connection.
528535
pub fn table_exists(conn: &Connection, table_name: &str) -> Result<bool, sqlite_error> {

testnet/stacks-node/src/node.rs

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use stacks::chainstate::stacks::events::{
1313
StacksTransactionEvent, StacksTransactionReceipt, TransactionOrigin,
1414
};
1515
use stacks::chainstate::stacks::{
16-
CoinbasePayload, StacksBlock, StacksBlockHeader, StacksMicroblock, StacksTransaction,
17-
StacksTransactionSigner, TransactionAnchorMode, TransactionPayload, TransactionVersion,
16+
CoinbasePayload, StacksBlock, StacksMicroblock, StacksTransaction, StacksTransactionSigner,
17+
TransactionAnchorMode, TransactionPayload, TransactionVersion,
1818
};
1919
use stacks::chainstate::{burn::db::sortdb::SortitionDB, stacks::db::StacksEpochReceipt};
2020
use stacks::core::mempool::MemPoolDB;
@@ -775,7 +775,7 @@ impl Node {
775775
microblocks: Vec<StacksMicroblock>,
776776
db: &mut SortitionDB,
777777
) -> ChainTip {
778-
let parent_consensus_hash = {
778+
let _parent_consensus_hash = {
779779
// look up parent consensus hash
780780
let ic = db.index_conn();
781781
let parent_consensus_hash = StacksChainState::get_parent_consensus_hash(
@@ -826,30 +826,6 @@ impl Node {
826826
parent_consensus_hash
827827
};
828828

829-
// get previous burn block stats
830-
let (parent_burn_block_hash, parent_burn_block_height, parent_burn_block_timestamp) =
831-
if anchored_block.is_first_mined() {
832-
(BurnchainHeaderHash([0; 32]), 0, 0)
833-
} else {
834-
match SortitionDB::get_block_snapshot_consensus(db.conn(), &parent_consensus_hash)
835-
.unwrap()
836-
{
837-
Some(sn) => (
838-
sn.burn_header_hash,
839-
sn.block_height as u32,
840-
sn.burn_header_timestamp,
841-
),
842-
None => {
843-
// shouldn't happen
844-
warn!(
845-
"CORRUPTION: block {}/{} does not correspond to a burn block",
846-
&parent_consensus_hash, &anchored_block.header.parent_block
847-
);
848-
(BurnchainHeaderHash([0; 32]), 0, 0)
849-
}
850-
}
851-
};
852-
853829
let atlas_config = AtlasConfig::default(false);
854830
let mut processed_blocks = vec![];
855831
loop {
@@ -930,26 +906,6 @@ impl Node {
930906
StacksChainState::consensus_load(&block_path).unwrap()
931907
};
932908

933-
let parent_index_hash = StacksBlockHeader::make_index_block_hash(
934-
&parent_consensus_hash,
935-
&block.header.parent_block,
936-
);
937-
938-
self.event_dispatcher.process_chain_tip(
939-
&block,
940-
&metadata,
941-
&receipts,
942-
&parent_index_hash,
943-
Txid([0; 32]),
944-
&vec![],
945-
None,
946-
parent_burn_block_hash,
947-
parent_burn_block_height,
948-
parent_burn_block_timestamp,
949-
&processed_block.anchored_block_cost,
950-
&processed_block.parent_microblocks_cost,
951-
);
952-
953909
let chain_tip = ChainTip {
954910
metadata,
955911
block,

0 commit comments

Comments
 (0)