Skip to content

Conversation

@sergerad
Copy link
Collaborator

Context

We need block validation to involve a check that all the transactions in a given block have previously been validated by the Validator.

We will begin by doing this in memory. Followup PR will add persistence.

Relates to #1316.

Changes

  • Update Validator transaction validation to store validated transaction headers.
  • Update Validator block validation to check for previously validated headers.

@sergerad sergerad changed the title feat: Add block validation logic against validated transactions feat: Add block validation logic against validated transactions (in-memory) Dec 17, 2025
@sergerad sergerad requested review from bobbinth and igamigo December 17, 2025 02:55
Comment on lines 51 to 57
// Remove the validated transactions from the cache.
let mut validated_txs = validated_transactions.write().await;
for tx_header in body.transactions().as_slice() {
validated_txs.remove(&tx_header.id());
}
// Release the validated transactions write lock.
drop(validated_txs);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest not manually managing the locks as it becomes easy to refactor your way into lock issues. How about replacing the lock type alias with a proper struct ValidatedTransactions instead?

#[derive(Default, Clone)]
struct ValidatedTransactions(RwLock<HashMap<TransactionId, TransactionHeader>>);

impl ValidatedTransactions {
    pub async fn check(&self, txs: impl Iterator<TransactionId>) -> Result<(), BlockValidationError>{
        let inner = self.0.read().await;

        ...
    }

    pub async fn remove(&self, txs: ...) {
        let inner = self.0.read().await;
        for tx in txs {
            inner.remove(tx);
        }
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Although thinking on this some more, I don't think we can actually remove these transactions at this stage.

We should only remove transactions once we know that the block has successfully landed in the store? Right now we're assuming that if this function is called that the caller successfully completes its end i.e. we're being optimistic.

Do we not need two methods, one for signing and one for the store to submit a BlockCommitted(TransactionIDs)?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would keep the transactions around for a long time. As I mentioned in my other comment, until we have persistence implemented, we could probably use something like an LRU cache (with fairly generous capacity) to keep transactions around. This way, we should be able to avoid the need for manual dropping.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added Lru impl

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I left some comments inline.

Comment on lines 32 to 33
/// A type alias for a read-write lock that stores validated transactions.
pub type ValidatedTransactions = RwLock<HashMap<TransactionId, TransactionHeader>>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we won't have the time to implement proper persistence for this before the next release. So, maybe this could be something like an LRU cache? We could set the size to be pretty big (e.g., thousands of transactions), but at least we'll be able to keep it bounded and we won't need to manually manage transaction eviction.

In the longer run, we probably want to store transactions for a long time for audibility purposes - but that's something to implement when we add persistence mechanism.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think we won't have the time to implement proper persistence for this before the next release

If we release the in-memory version won't we risk causing a halt if we restart the node? Which is to say that we can't release this until persistence is in place.

Copy link
Collaborator

Choose a reason for hiding this comment

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

imo just adding a validator.sqlite with a basic

CREATE TABLE transactions (
    id BLOB PRIMARY KEY,
    header BLOB NOT NULL,
    body BLOB NOT NULL,
    timestamp INTEGER NOT NULL,
);

is probably quicker to do than the time we spend thinking and discussing this :)

The "hardest" part will be the cli configuration and briefly thinking about how/if concurrent writes are a concern.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There was also the issue of code factoring because the validator db wouldn't go through the store. I had a PR a while ago that implemented a basic approach for it. I assume we are still of the mind that it doesn't make sense for the validator db to be tied to the store, but I haven't thought about it for a while now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be its own separate database

Comment on lines 51 to 57
// Remove the validated transactions from the cache.
let mut validated_txs = validated_transactions.write().await;
for tx_header in body.transactions().as_slice() {
validated_txs.remove(&tx_header.id());
}
// Release the validated transactions write lock.
drop(validated_txs);
Copy link
Contributor

Choose a reason for hiding this comment

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

I would keep the transactions around for a long time. As I mentioned in my other comment, until we have persistence implemented, we could probably use something like an LRU cache (with fairly generous capacity) to keep transactions around. This way, we should be able to avoid the need for manual dropping.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I left one small comment inline.

Comment on lines +32 to +33
/// Number of transactions to keep in the validated transactions cache.
const NUM_VALIDATED_TRANSACTIONS: NonZeroUsize = NonZeroUsize::new(7000).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's bump this up to 10K transactions.

@sergerad sergerad merged commit d280f56 into next Jan 5, 2026
18 checks passed
@sergerad sergerad deleted the sergerad-validator-block-tx-validation branch January 5, 2026 21:59
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.

5 participants