Skip to content

Tapes db#587

Open
Boog900 wants to merge 27 commits intomainfrom
new-db
Open

Tapes db#587
Boog900 wants to merge 27 commits intomainfrom
new-db

Conversation

@Boog900
Copy link
Copy Markdown
Member

@Boog900 Boog900 commented Feb 21, 2026

No description provided.

@github-actions github-actions bot added A-test-utils Area: Related to test-utils. A-dependency Area: Related to dependencies, or changes to a Cargo.{toml,lock} file. A-workspace Area: Changes to a root workspace file or general repo file. A-consensus Area: Related to consensus. A-docs Area: Related to documentation. A-storage Area: Related to storage. A-helper Area: Related to cuprate-helper. A-pruning Area: Related to pruning. A-types Area: Related to types. A-binaries Area: Related to binaries. labels Feb 21, 2026
@github-actions github-actions bot added the A-p2p Area: Related to P2P. label Feb 24, 2026
@github-actions github-actions bot added the A-ci Area: Related to CI. label Feb 25, 2026
@hinto-janai hinto-janai added this to the cuprated v0.0.9 milestone Feb 26, 2026
@Boog900
Copy link
Copy Markdown
Member Author

Boog900 commented Mar 4, 2026

I think this is about done. This was a massive task but the outcome is great, the database is now small and faster both reading and writing (practically, some operations on some tables may be slower).

This is probably going to take some time to review, the old docs have all been removed as I didn't want to spend the time updating it all while everything is still changing. Its the same with the lower level tests, the higher level tests are still present.

The format of the code is still roughly the same.

@Boog900 Boog900 marked this pull request as ready for review March 4, 2026 02:56
@Boog900 Boog900 requested a review from hinto-janai March 4, 2026 02:56
Copy link
Copy Markdown
Contributor

@redsh4de redsh4de left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some comments

Comment thread storage/blockchain/src/ops/alt_block/block.rs Outdated
Comment thread storage/blockchain/src/ops/output.rs Outdated
Comment thread storage/txpool/src/service/write.rs Outdated
Comment thread storage/blockchain/src/service/read.rs Outdated
Comment thread consensus/src/block.rs Outdated
Comment thread binaries/cuprated/src/config.rs Outdated
#[inline]
pub fn remove_tx(tx_hash: &TxHash, tables: &mut impl TablesMut) -> DbResult<(TxId, Transaction)> {
//------------------------------------------------------ Transaction data
let tx_id = tables.tx_ids_mut().take(tx_hash)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main used to have .take() which deletes the tx_hash, i dont see the remove_tx_from_dynamic_tables deleting the tx_hash from tx_ids - only key images and outputs

edit: looks like it impacts add_alt_transaction_blob in src/ops/alt_block/tx.rs as well - sees tx hash exists, skips adding the blob

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops forgot to add the explicit removal when I move to a write batch instead of an atomic tx, should be fixed now

Comment thread consensus/fast-sync/src/create.rs Outdated
Comment thread storage/blockchain/src/ops/alt_block/block.rs Outdated
Comment thread binaries/cuprated/src/blockchain/manager/handler.rs
Comment thread storage/blockchain/src/ops/alt_block/tx.rs Outdated
Copy link
Copy Markdown
Contributor

@redsh4de redsh4de left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly nitpicks

Comment thread storage/blockchain/src/ops/output.rs
Comment thread storage/blockchain/src/ops/output.rs
monero_oxide::io::VarInt::write(&block_txs.len(), &mut block)
.expect("The number of txs per block will not exceed u64::MAX");
let cumulative_rct_outs = tapes
.read_entry(&db.block_infos, block_height as u64 - 1)?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, to match L215

Suggested change
.read_entry(&db.block_infos, block_height as u64 - 1)?
.read_entry(&db.block_infos, block_height.saturating_sub(1) as u64)?

Comment on lines +581 to +584
let top_block_height = tapes
.fixed_sized_tape_len(&db.block_infos)
.expect("Required tape not open")
- 1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style nit: could port the same defensive check from chain_height

unnecessary as genesis block always gets added but good for consistency

Suggested change
let top_block_height = tapes
.fixed_sized_tape_len(&db.block_infos)
.expect("Required tape not open")
- 1;
let chain_height = tapes
.fixed_sized_tape_len(&db.block_infos)
.expect("Required tape not open");
if chain_height == 0 {
return Err(BlockchainError::NotFound);
}
let top_block_height = chain_height - 1;

block_height(db, &tx_ro, &first_known_block_hash)?.ok_or(BlockchainError::NotFound)?;

let chain_height = crate::ops::blockchain::chain_height(table_block_heights)?;
let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Suggested change
let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
if chain_height == 0 {
return Err(BlockchainError::NotFound):
}

let height = u64_to_usize(
tapes
.fixed_sized_tape_len(&db.block_infos)
.expect("Require tape not found")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its over literally unshippable

Suggested change
.expect("Require tape not found")
.expect("Required tape not found")

table_block_heights.len().map(|height| height as usize)
Ok(tapes
.fixed_sized_tape_len(&db.block_infos)
.expect("Required tape must exists") as usize)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too

Suggested change
.expect("Required tape must exists") as usize)
.expect("Required tape must exist") as usize)

Comment on lines +147 to +165
tapes.commit(Persistence::Buffer)?;

let mut pre_rct_numb_outputs_cache = db.pre_rct_numb_outputs_cache.lock().unwrap();

let mut tx_rw = db.fjall.batch().durability(Some(PersistMode::Buffer));

for block in blocks {
crate::ops::block::add_block_to_dynamic_tables(
db,
&block.block,
&block.block_hash,
block.txs.iter().map(|tx| Cow::Borrowed(&tx.tx)),
&mut numb_transactions,
&mut tx_rw,
&mut pre_rct_numb_outputs_cache,
)?;
}

tx_rw.commit()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: with graceful shutdown all errors after tapes commit, but before fjall successfully commits should be distinguished due to us being out of sync after that. BlockchainError::DatabaseOutOfSync?

handler.rs does panic on error now so not a issue rn, but graceful shutdown changes this

idea: recovery function that replays only the out of sync blocks from tapes into fjall when we get that kind of error instead instead of a full chain nuke on desync

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was going to do that originally, however full fjall nuke is much easier to handle + is not too slow. You can test it by just deleting the fjall DB but keeping the tapes.

Comment on lines +39 to +44
/// - The blockchain has 0 blocks => this returns `Err(BlockchainError::KeyNotFound)`
/// - The blockchain has 1 block (height 0) => this returns `Ok(0)`
/// - The blockchain has 2 blocks (height 1) => this returns `Ok(1)`
///
/// Note that in cases where no blocks have been written to the
/// database yet, an error is returned: `Err(RuntimeError::KeyNotFound)`.
/// database yet, an error is returned: `Err(BlockchainError::KeyNotFound)`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyNotFound -> NotFound

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think you care to change this but in the storage/ diffs there are many u32/u64/usize as ops that could use cuprate_helper::cast already in scope.

// Split the blocks at the point the pruning stripe changes.
let start_height = blocks[0].height;
let first_block_pruning_seed = cuprate_pruning::DecompressedPruningSeed::new(
cuprate_pruning::get_block_pruning_stripe(start_height, usize::MAX, 3).unwrap(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

rg "usize::MAX, 3"

storage/blockchain/src/ops/tx.rs
259:            cuprate_pruning::get_block_pruning_stripe(tx_info.height, usize::MAX, 3).unwrap();

storage/blockchain/src/ops/block.rs
111:        cuprate_pruning::get_block_pruning_stripe(start_height, usize::MAX, 3).unwrap(),
143:            cuprate_pruning::get_block_pruning_stripe(blocks[0].height, usize::MAX, 3).unwrap();
405:    let stripe = cuprate_pruning::get_block_pruning_stripe(block_height, usize::MAX, 3).unwrap();
// cuprate_pruning

pub fn get_default_block_pruning_stripe(start_height) -> u32 {
    get_block_pruning_stripe(
        block_height,
        usize::MAX,
        CRYPTONOTE_PRUNING_LOG_STRIPES
    ).unwrap()
}


/// Adds a [`VerifiedTransactionInformation`] from an alt-block
/// if it is not already in the DB.
/// Adds a [`VerifiedTransactionInformation`] from an alt-block if it is not already in the DB.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there's probably a lot of inaccurate docs at this point; this function replaces it unconditionally.

Comment on lines +413 to +414
let cumulative_rct_outs = tapes
.read_entry(&db.block_infos, block_height as u64 - 1)?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#587 (comment)

Call stack:

  • cuprated::blockchain::manager::pop_blocks
  • BlockchainWriteRequest::PopBlocks
  • cuprate_blockchain::service::write::pop_blocks
  • cuprate_blockchain::ops::block::pop_blocks

AFAICT input that can pop the genesis is allowed? I think this will underflow.

/// **THIS IS NOT ATOMIC**
///
pub fn update_alt_chain_info(
db: &BlockchainDatabase,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be given a tx_rw: &mut fjall::OwnedWriteBatch? It is only called from add_alt_block which has it open already.

Comment on lines +183 to +200
fn pop_blocks(db: &BlockchainDatabase, numb_blocks: usize) -> ResponseResult {
let mut tapes = db.linear_tapes.truncate();
let mut tx_rw = db.fjall.batch();

// flush all the current alt blocks as they may reference blocks to be popped.
crate::ops::alt_block::flush_alt_blocks(db)?;

// generate a `ChainId` for the popped blocks.
let old_main_chain_id = ChainId(rand::random());

// pop the blocks
for _ in 0..numb_blocks {
crate::ops::block::pop_block(db, Some(old_main_chain_id), &mut tx_rw, &mut tapes)?;
}

tx_rw.commit()?;
tapes.commit(Persistence::SyncAll)?;
Ok(BlockchainResponse::PopBlocks(old_main_chain_id))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't tx_rw be passed to flush_alt_blocks?

pub(crate) linear_tapes: Tapes,
pub(crate) fjall: fjall::Database,

pub(crate) block_heights: fjall::Keyspace,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional but perhaps (K,V) and their encoding should be documented since the types/tables are gone, even monerod's DB is easier to understand at a glance now 😭

Suggested change
pub(crate) block_heights: fjall::Keyspace,
/// K = `[u8; 32]` (block hash), V = [`usize`], encoding = little endian.
pub(crate) block_heights: fjall::Keyspace,

.build()
cuprate_blockchain::config::Config {
blob_dir: path_with_network(&self.fs.fast_data_directory, self.network),
index_dir: path_with_network(&self.fs.slow_data_directory.clone(), self.network),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
index_dir: path_with_network(&self.fs.slow_data_directory.clone(), self.network),
index_dir: path_with_network(&self.fs.slow_data_directory, self.network),

cache_sizes below can be Copy but meh

/// | macOS | "/Users/Alice/Library/Application Support/Cuprate/" |
/// | Linux | "/home/alice/.local/share/cuprate/" |
pub data_directory: PathBuf,
pub fast_data_directory: PathBuf,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to split this into two? They're both the same path.

Comment on lines +170 to +173
db.tx_infos.insert(tx_hash, bytemuck::bytes_of(&tx_info))?;

if !db.tx_blobs.contains_key(tx_hash)? {
db.tx_infos.remove(tx_hash)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be through a OwnedWriteBatch?

@hinto-janai
Copy link
Copy Markdown
Member

I still don't have a good mental model of the atomic-ness between fjall/tapes, there are separate transactions everywhere, sometimes directly into a keyspace, sometimes in different commit orders. It doesn't seem like we can have a deterministic safe sync mode (as much as monerod is). Is this just a tradeoff being taken?

@Boog900
Copy link
Copy Markdown
Member Author

Boog900 commented Apr 15, 2026

The DB is now 2 systems that are for ACID purposes independent. The tapes DB will do 1 update atomically, so will fjall, the 2 updates combined are not atomic.

This does lead to issues with the fact they could get out of sync. On start-up we solve this by using the tapes DB as the source of truth, if fjall is out of sync, we rebuild it from the tapes (doesn't take that long for me). On some requests where we need an fjall and tapes TX to be in sync we should check if they are then just open another set of txs until they are in sync (this is not done yet).

The only exception is alt blocks, for which fjall is not atomic. This is ok as we clear alt blocks on startup anyway and request should be built to handle only some alt block data for a given block being present.

This all means Cuprated should be able to have a safe sync mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-binaries Area: Related to binaries. A-ci Area: Related to CI. A-consensus Area: Related to consensus. A-dependency Area: Related to dependencies, or changes to a Cargo.{toml,lock} file. A-docs Area: Related to documentation. A-helper Area: Related to cuprate-helper. A-p2p Area: Related to P2P. A-pruning Area: Related to pruning. A-storage Area: Related to storage. A-test-utils Area: Related to test-utils. A-types Area: Related to types. A-workspace Area: Changes to a root workspace file or general repo file.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants