Skip to content

feat(iota-core): Post consensus load shedding#11301

Open
cyberphysic4l wants to merge 18 commits into
consensus/feat/pcool-featurefrom
protocol-research/feat/post-consensus-load-shedding
Open

feat(iota-core): Post consensus load shedding#11301
cyberphysic4l wants to merge 18 commits into
consensus/feat/pcool-featurefrom
protocol-research/feat/post-consensus-load-shedding

Conversation

@cyberphysic4l
Copy link
Copy Markdown
Contributor

@cyberphysic4l cyberphysic4l commented Apr 23, 2026

Description of change

Adds a post-consensus load shedding path for the certificate-less (white-flag) flow. Validators now broadcast their measured load shedding percentage to peers via a new consensus transaction, and each validator deterministically drops user transactions during post-consensus categorization based on the stake-weighted quorum percentile of those load shedding percentages — so the entire committee sheds the same set of transactions in the same round.

Prior to white-flag changes, the load shedding percentage was only used locally and was based on execution queue latency. There was also a transactions manager queue based threshold and writeback cache threshold above which everything was dropped. Now the previous latency based load shedding percentage is combined with a transaction manager queue and writeback cache based load shedding percentage, and all the dropping for these is done after consensus.

How the change has been tested

  • Basic tests (linting, compilation, formatting, unit/integration tests)
  • Patch-specific tests (correctness, functionality coverage)
  • I have added tests that prove my fix is effective or that my feature works
  • I have checked that new and existing unit tests pass locally with my changes

@cyberphysic4l cyberphysic4l force-pushed the protocol-research/feat/post-consensus-load-shedding branch from a827011 to 7464fd6 Compare April 24, 2026 12:00
@cyberphysic4l cyberphysic4l changed the base branch from develop to protocol-research/feat/certificate-less-pre-consensus-load-shedding April 24, 2026 12:01
@cyberphysic4l cyberphysic4l changed the title Protocol research/feat/post consensus load shedding feat(iota-core): Post consensus load shedding Apr 27, 2026
@vekkiokonio vekkiokonio linked an issue Apr 27, 2026 that may be closed by this pull request
@roman1e2f5p8s roman1e2f5p8s force-pushed the protocol-research/feat/certificate-less-pre-consensus-load-shedding branch from 0cb3220 to 0f921e7 Compare April 29, 2026 07:57
@roman1e2f5p8s roman1e2f5p8s force-pushed the protocol-research/feat/post-consensus-load-shedding branch from 5a62a09 to 8bf43a5 Compare April 29, 2026 12:26
Comment thread crates/iota-core/src/overload_monitor.rs Outdated
@cyberphysic4l cyberphysic4l force-pushed the protocol-research/feat/post-consensus-load-shedding branch from 8bf43a5 to 0808cfd Compare April 30, 2026 15:23
@cyberphysic4l cyberphysic4l marked this pull request as ready for review May 1, 2026 14:13
@cyberphysic4l cyberphysic4l requested review from a team as code owners May 1, 2026 14:13
Comment thread crates/iota-types/src/messages_consensus.rs Outdated
let mut weighted_values: Vec<(u8, StakeUnit)> = committee
.members()
.map(|(authority, stake)| {
let percentage = notifications.get(authority).copied().unwrap_or(0);
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.

Validators that haven't sent a notification default to 0%. A malicious validator could stay silent to keep the quorum shedding percentage low. Maybe require periodic heartbeats even when unchanged? Or do I miss something?

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.

but again - what do you do if somebody doesn't send the heartbeat? you treat it as zero?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A malicious validator could also simply broadcast a value of 0 and achieve the same effect, nothing enforces particular values to be sent, it is all based on local an unprovable measurements. The security against malicious behaviour comes from the fact that the stake weighted quorum percentile is used to compute the effective load shedding value, limiting the ability of malicious validators to bias the load shedding percentage.

.overload_info
.load_shedding_percentage
.load(std::sync::atomic::Ordering::Relaxed);
if current != last_notified_percentage {
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.

This only sends when the percentage changes. New validators or validators recovering from partitions won't get fresh data if the system is stable. Maybe send a heartbeat every N intervals even when unchanged?

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.

This data is only relevant during an epoch and the overload monitor is reset at the epoch boundary. I don't think it's necessary to issue every N intervals with the same status. If a node has restarted and is catching up from either clean or outdated consensus database, then all of the overload notifications live inside the DAG anyway, and will be process by consensus_handler in the correct order anyway. Could you describe the scenario where not issuing at regular intervals would be problematic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is an important concern that you've highlighted and I have been trying to figure out exactly how different new validator or crash and recovery situations will work. If I understand correctly now, validators will always process all commits in order, and when processing a new commit (where load shedding is done) the epoch store tables will always reflect the latest overload notification from each authority. If there was no notificiation in the epoch, the value is zero. So getting fresh data is not a concern in the sense that the data comes from commits and we are guaranteed to have fresh commit data when we need it.

Although getting fresh data from commits is not a problem, broadcasting fresh data about local conditions may be what you mean. So if a validator was overloaded when it crashed and hence the committed overload notification for this validator has a high load shedding percentage, they may not update this by broadcasting a new value when they recover from the crash. I think this may be better solved by broadcasting (even a zero value) upon start up to make sure each validator has put out a fresh value since last starting up. Does that make sense or do you think there is another reason for having a heartbeat-style approach to broadcasting?

Comment thread crates/iota-config/src/node.rs Outdated
Comment thread crates/iota-config/src/node.rs Outdated
Comment thread crates/iota-core/src/authority/authority_per_epoch_store.rs Outdated
Comment thread crates/iota-core/src/authority/authority_per_epoch_store.rs Outdated
// quorum load shedding percentage.
if drop_percentage > 0 {
if let Some(digest) = tx.0.transaction.executable_transaction_digest() {
if should_reject_tx(drop_percentage, digest, drop_seed) {
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.

should we make sure to not drop UserTransactions such as OverloadNotification or AuthorityCapabilities? OverloadNotification is more crucial I think. Or is that handled someplace else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was actually handled already, executable_transaction_digest() would return digests only for user originating transactions and system transactions, but system transactions already dealt with in earlier match arm. I've made it more explicit now by having the user_transaction_digest() method which will only return digests for user originated transaction, so OverloadNotification and AuthorityCapabilities etc will never be dropped by this load shedding.

.overload_info
.load_shedding_percentage
.load(std::sync::atomic::Ordering::Relaxed);
if current != last_notified_percentage {
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.

This data is only relevant during an epoch and the overload monitor is reset at the epoch boundary. I don't think it's necessary to issue every N intervals with the same status. If a node has restarted and is catching up from either clean or outdated consensus database, then all of the overload notifications live inside the DAG anyway, and will be process by consensus_handler in the correct order anyway. Could you describe the scenario where not issuing at regular intervals would be problematic?

kind: ConsensusTransactionKind::OverloadNotificationV1(authority, percentage),
..
}) => {
if &transaction.sender_authority() != authority {
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.

validate_transactions_feature_gating - does this test pass if there is no feature gating on this transaction kind? Perhaps the test is missing something, but in general we don't want to allow malicious validators to issue a transaction kind before it's supported by a feature flag despite the software version being able to deserialize it. Say, we merge this to develop and release, but mainnet/testnet does not have pcool enabled. Then all nodes would try to process this transaction kind (and potentially crash) without protocol supporting this feature. That's what the validate_transactions_feature_gating test was supposed to catch, so maybe it's actually gated somehow, but I figured I'd ask anyway just to make sure.

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.

Ok, I see this is checked in consensus_validator.rs, but I added explicit check here for SignedCapabilityNotification and there is a similar TODO for UserTransactionV1, so I guess we should clean this up at some point.

let mut weighted_values: Vec<(u8, StakeUnit)> = committee
.members()
.map(|(authority, stake)| {
let percentage = notifications.get(authority).copied().unwrap_or(0);
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.

but again - what do you do if somebody doesn't send the heartbeat? you treat it as zero?

"Percentage of transactions shed due to consensus queue length.",
registry)
.unwrap(),
cache_backpressure_load_shedding_percentage: register_int_gauge_with_registry!(
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.

we should add metrics to count how many transactions were actually shedded post-consensus. Optionally we should count how many notifications we've received from each authority, but I'm not sure if that's worth polluting the metrics (we have too many already 😅).

@roman1e2f5p8s roman1e2f5p8s force-pushed the protocol-research/feat/certificate-less-pre-consensus-load-shedding branch from 014f91c to c520090 Compare May 7, 2026 09:51
@roman1e2f5p8s roman1e2f5p8s requested review from a team as code owners May 7, 2026 09:51
@roman1e2f5p8s roman1e2f5p8s force-pushed the protocol-research/feat/certificate-less-pre-consensus-load-shedding branch from c520090 to 8afda17 Compare May 7, 2026 12:40
@cyberphysic4l cyberphysic4l force-pushed the protocol-research/feat/post-consensus-load-shedding branch from 3d4b491 to 76e20ea Compare May 7, 2026 15:46
@roman1e2f5p8s roman1e2f5p8s removed request for a team May 7, 2026 17:06
@cyberphysic4l cyberphysic4l force-pushed the protocol-research/feat/post-consensus-load-shedding branch from 76e20ea to aabd9b6 Compare May 11, 2026 14:27
@cyberphysic4l cyberphysic4l changed the base branch from protocol-research/feat/certificate-less-pre-consensus-load-shedding to consensus/feat/transaction-certificateless-flow-feature May 11, 2026 14:29
Copy link
Copy Markdown
Contributor

@roman1e2f5p8s roman1e2f5p8s left a comment

Choose a reason for hiding this comment

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

one test I would add is to exercise the actual drop behavior inside process_consensus_transactions_and_commit_boundary.
see test_consensus_commit_prologue_generation at authority_tests.rs:4671 for how such a test could be constructed

}
}

if tx.0.is_user_tx_with_randomness() {
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.

Suggested change
if tx.0.is_user_tx_with_randomness() {
if !enable_white_flag && tx.0.is_user_tx_with_randomness() {

see #10764 why it should NOT be removed.

Comment on lines +301 to +304
let mut last_notified_percentage: u32 = state
.overload_info
.local_load_shedding_percentage
.load(std::sync::atomic::Ordering::Relaxed);
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.

shouldn't last_notified_percentage be initialized from the persisted view instead of the atomic on startup? something like

let mut last_notified_percentage: u32 = epoch_store
    .load_overload_notifications()
    .expect("...")
    .get(&authority_name)
    .copied()
    .map(u32::from)
    .unwrap_or(0);

Comment on lines +126 to +134
ConsensusTransactionKind::OverloadNotificationV1(_, _) => {
if !self.epoch_store.protocol_config().enable_white_flag_flow() {
return Err(IotaError::UnsupportedFeature {
error:
"OverloadNotificationV1 not supported at current protocol version"
.into(),
});
}
}
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.

Suggested change
ConsensusTransactionKind::OverloadNotificationV1(_, _) => {
if !self.epoch_store.protocol_config().enable_white_flag_flow() {
return Err(IotaError::UnsupportedFeature {
error:
"OverloadNotificationV1 not supported at current protocol version"
.into(),
});
}
}
ConsensusTransactionKind::OverloadNotificationV1(authority_name, percentage) => {
if !self.epoch_store.protocol_config().enable_white_flag_flow() {
return Err(IotaError::UnsupportedFeature {
error:
"OverloadNotificationV1 not supported at current protocol version"
.into(),
});
}
if *percentage > 100 {
return Err(IotaError::HandleConsensusTransactionFailure(format!(
"OverloadNotificationV1 with invalid percentage {percentage} from \
authority {}",
authority_name.concise(),
)));
}
}

not just feature gate, but worth doing a cheap check - some other variants do that here.

@cyberphysic4l cyberphysic4l force-pushed the protocol-research/feat/post-consensus-load-shedding branch from e093081 to c9e6b55 Compare May 21, 2026 09:06
@cyberphysic4l cyberphysic4l changed the base branch from consensus/feat/transaction-certificateless-flow-feature to consensus/feat/pcool-feature May 21, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pcool: PoC post-consensus load shedding

7 participants