Skip to content

[WIP] feat: Assume-Valid SwiftSync with accumulator building#1115

Draft
JoseSK999 wants to merge 5 commits into
getfloresta:masterfrom
JoseSK999:assume-valid-swiftsync-parallel-stump
Draft

[WIP] feat: Assume-Valid SwiftSync with accumulator building#1115
JoseSK999 wants to merge 5 commits into
getfloresta:masterfrom
JoseSK999:assume-valid-swiftsync-parallel-stump

Conversation

@JoseSK999
Copy link
Copy Markdown
Member

Description and Notes

This is a more complete version of #837, as we have Utreexo accumulator building during SwiftSync.

This means your node will perform SwiftSync and build the Utreexo accumulator at the same time, which is cheap thanks to "implicit deletion". After SwiftSync we can finally continue with the usual Utreexo operation, as the UTXO set represented by the accumulator has been verified.

Implicit deletion (implemented in mit-dci/rustreexo#81)

We only need Utreexo proofs (Merkle inclusion proofs) when we need to verify an UTXO exists, and then remove it from the set. Nonetheless, in this case we have SwiftSync hints for spent-ness, so we can delete the hinted-as-spent TXOs at addition time. The later Merkle proof step is skipped entirely, which is a huge bandwidth cut (and avoids hashing to verify the proof).

How to run

You need the hints for the network you intend to run, named {network}.hints (for example, bitcoin.hints or signet.hints), located in the same datadir as your node uses. Default directory is .floresta in the root for mainnet (and .floresta/signet, etc.). Hintsfiles for different networks can be downloaded from the 2140-dev server.

For both mainnet and signet you need to run florestad with assume-utreexo disabled:

cargo run --release -- --no-assume-utreexo
cargo run --release -n signet -- --no-assume-utreexo
  • Note: if no SwiftSync hints are found for the current network, florestad falls back to a full SyncNode sync (i.e., the full Merkle-proof-heavy and sequential Utreexo sync).
  • Note_2: currently this runs Witnessless SwiftSync, and then switches to downloading full blocks.

StumpUpdater

To keep SwiftSync block processing fully parallel while still producing a Stump (the accumulator type), we decouple block processing from accumulator updates. The accumulator updates happen in a separate worker, the StumpUpdater from stump_updater.rs.

StumpUpdater receives the UTXO hashes to add, linked to the height they should be applied on. It keeps a cache of pending updates, and enforces the order of updates is sequential.

TODO: how big can this cache be, and how could we restrict its size?

pub struct StumpUpdater {
    /// The accumulator for `last_height`.
    last_acc: Stump,

    /// The last height we have processed. This is always incremented by 1, iff we have the update
    /// data for the next height.
    last_height: u32,

    /// Pending additions, deletions, and proofs to apply to the accumulator, mapped to the height
    /// at which they must be applied.
    pending_updates: BTreeMap<u32, StumpUpdate>,
}

Once we reach the SwiftSync stop_height (and verify the hints were correct), this worker returns the final computed accumulator, and we continue with the Utreexo-proof mode.

let inv = block_hashes
.iter()
.map(|block| Inventory::Block(*block))
.map(|block| match witnessless {
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.

Why not if ... else?

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.

I think when I wrote this it was more concise this way

self.send_to_peer(
peer,
NodeRequest::GetBlock(vec![header.block_hash()]),
NodeRequest::GetBlock(vec![header.block_hash()], false),
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.

These callsites can become confusing to read without having further context about NodeRequest. I think this is an example of C-CUSTOM-TYPE whereby we should define a new enum (something like WitnessData::Include/Exclude) so the intent is clear from the callsite

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.

Agree, good idea!

Comment thread Cargo.toml
codegen-units = 1 # compile a single codegen unit

[patch.crates-io]
rustreexo = { git = "https://github.com/Davidson-Souza/rustreexo.git", branch = "swift-sync" }
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.

Not a review comment but this is a cool feature I did not know about

@JoseSK999
Copy link
Copy Markdown
Member Author

The test_swift_sync_mutated_block failure has revealed that we don't re-request blocks that were asked to a now banned peer (in this case we banned it because block txid didn't match).

I think we need to harden how we redo inflight requests for this case, need to take a deeper look.

@Davidson-Souza
Copy link
Copy Markdown
Member

It doesn't recover the swift sync progress after clean shutdown. Also couldn't use the node handle when syncing.

Comment on lines +11 to +16
/// Pending additions, deletions, and proof for a single accumulator update.
pub struct StumpUpdate {
pub adds: Vec<BitcoinNodeHash>,
pub deletes: Vec<BitcoinNodeHash>,
pub proof: Proof,
}
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.

You intend to use this with SyncNode as well?

With Swift Sync you can simplify this and send the UTXOs being added + the hints for this block. So you only inflate the zero hashes when processing that block. This should reduce the memory footprint by a lot.

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.

For sync node we could perhaps make a proof verifier as well, so the pipeline goes:

Proof verifier -> block checker (all but utreexo stuff) -> stump updater.

To feed the proof verifier out of order we can use the get utreexo roots message and fetch roots from peers.

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.

Yes I made it generic over all the update data needed for Utreexo, because this may be useful outside of SS.

Skipping the zero hashes in the cache is a very good idea! Probably it will eliminate most of the cache data.

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.

Proof verifier -> block checker (all but utreexo stuff) -> stump updater.

Makes sense because then the stump updater can just apply an infallible operation no? (add UTXOs, remove proved ones)

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.

Yes I made it generic over all the update data needed for Utreexo, because this may be useful outside of SS.

Maybe you can make an enum to distinguish between Swift Sync and non-swift sync

Probably it will eliminate most of the cache data.

Very likely, the overwhelming majority of TXOs are already spent, so no need to have them there.

Copy link
Copy Markdown
Member

@Davidson-Souza Davidson-Souza Jun 3, 2026

Choose a reason for hiding this comment

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

Makes sense because then the stump updater can just apply an infallible operation no?

I'm pretty sure it is.

Edit: And we can have more than one proof checker worker. This is the second most CPU-intensive task for no-assumevalid blocks, after script validation.

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.

Before checking the proof you need the block anyway no? I was thinking about verifying the proof and block in the same “validation” worker.

It shouldn’t matter if the parallel workers do 2x work, because we will have all workers busy.

@JoseSK999
Copy link
Copy Markdown
Member Author

JoseSK999 commented Jun 3, 2026

It doesn't recover the swift sync progress after clean shutdown. Also couldn't use the node handle when syncing.

Yeah we don't save the agg progress, we would need to save a bitmap for the heights that we have processed and persist the agg. Edit: oh and we need to persist the ChainStore progress if that's what you meant. Idk about the node handle yet.

@Davidson-Souza
Copy link
Copy Markdown
Member

~2h on my VPS to sync 🚀🚀🚀

@Davidson-Souza
Copy link
Copy Markdown
Member

Sometimes I lose all my peers and just get stuck.

@JoseSK999
Copy link
Copy Markdown
Member Author

Sometimes I lose all my peers and just get stuck.

Do you see logs like “received a block we didn’t request” or similar? Or they simply disconnect? It may be that we over-request to peers. Capping the number of inflight requests per peer may solve it.

@Davidson-Souza
Copy link
Copy Markdown
Member

Sometimes I lose all my peers and just get stuck.

Do you see logs like “received a block we didn’t request” or similar? Or they simply disconnect? It may be that we over-request to peers. Capping the number of inflight requests per peer may solve it.

They got banned. But FYI I've increased the max requests to test something.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants