Yesterday, hardfork-beta activated at block height 38,000. With this hardfork, a lustration barrier was introduced. All UTXOs generated prior to the activation of this hardfork have to go through the barrier.
The barrier works such that if the UTXO is older than a certain cutoff, then the transaction needs to reveal the value of the UTXO. This is done by including an announcement, that reveals the UTXO being spent, into the transaction. When the value of the UTXO is revealed, the amount contained in the UTXO can be subtracted from the global lustration counter. If the counter never becomes zero, we can guarantee that a previous soundness bugs in Triton VM has not inflated the money supply beyond its intended value.
However, the code to update the counter has a small bug that could lead to some amounts being lustrated twice – i.e. being subtracted twice from the counter. If UTXOs were spent immediately after the hardfork and then respent before ~150 outputs had been created after the hardfork, then those UTXOs could be subtracted from the counter twice.
Because the founders were aware of this problem when the hardfork activated yesterday evening, they created multiple high fee paying transactions with many outputs of low value such that this window was closed as quickly as possible. These transactions with many outputs were mined in blocks 38,002, 38,004, 38,006, effectively closing the window.
Because of the lustration requirement, we can put an upper limit of the value of the UTXOs being lustrated while this problematic window was open, and thus an upper limit on the value of the UTXOs that could be double-counted. The lustration counter’s value started at 8,423,168 NPT at block height 38,000. At block height 38,006 the lustration counter’s value had dropped to 8,201,138, meaning that ~200,000 coins were lustrated during this window. Since more than 1.5m coins have been burned prior to the hardfork, these double-counted UTXOs are not a problem.
For anyone interested, the code can be found in [`neptune-core/src/protocol/consensus/transaction/transaction_kernel.rs`]( neptune-core/neptune-core/src/protocol/consensus/transaction/transaction_kernel.rs at master · Neptune-Crypto/neptune-core · GitHub ):
pub(crate) fn verified_lustration_amount(
&self,
max_lustrating_aocl_leaf_index: u64,
) -> Result<NativeCurrencyAmount, TransactionLustrationError> {
let mut required_lustrations = vec![];
for input in &self.inputs {
let Ok((input_index_lower_end, _)) = input.absolute_indices.aocl_range() else {
return Err(TransactionLustrationError::InvalidAoclRangeForIndexSet);
};
if input_index_lower_end <= max_lustrating_aocl_leaf_index {
required_lustrations.push(input.absolute_indices);
}
}
let mut required_lustrations: HashSet<_> = required_lustrations.into_iter().collect();
let all_lustrations = self
.announcements
.iter()
.filter(|ann| {
ann.message
.first()
.is_some_and(|elem0| *elem0 == LUSTRATION_FLAG)
})
.collect_vec();
let mut acc_amount = NativeCurrencyAmount::zero();
for lustration in all_lustrations {
let Ok(lustration) = TransparentInput::decode(&lustration.message[1..]) else {
continue;
};
let implied_index_set = lustration.absolute_index_set();
let was_present = required_lustrations.remove(&implied_index_set);
// below line should have been: if was_present && lustration.aocl_leaf_index <= max_lustrating_aocl_leaf_index
if was_present {
acc_amount += lustration.utxo.get_native_currency_amount();
}
}
if !required_lustrations.is_empty() {
return Err(TransactionLustrationError::MissingLustrationAnnouncement);
}
Ok(acc_amount)
}
The above analysis shows that this bug will not lead to a negative lustration counter and will thus not lead to loss of funds (through a negative counter), since much more has been burnt than will potentially be double counted.
Because this is consensus code, and a fix would require a hardfork and a rewrite of history and because the bug carries no risk of loss-of-funds, this bug will not be fixed. The window for exploiting the bug has already been closed.