use alloy::{primitives::B256, sol_types::SolError};
use newton_prover_chainio::error::ChainIoError;
use newton_prover_core::state_commit_registry::StateCommitRegistry::{
CertificateMessageHashMismatch, InvalidNewStateRoot, InvalidPcr0Commitment, InvalidSealedSnapshot, SequenceGap,
StateRootMismatch, TimestampRegression, UnsupportedStateCommitVersion,
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StateCommitError {
#[error("StateCommitRegistry address not configured for this chain")]
RegistryNotConfigured,
#[error("registry sequence gap: expected={expected}, got={got}")]
SequenceGap {
expected: u64,
got: u64,
},
#[error("state root mismatch: expected={expected}, got={got}")]
StateRootMismatch {
expected: B256,
got: B256,
},
#[error("timestamp regression: last={last}, got={got}")]
TimestampRegression {
last: u64,
got: u64,
},
#[error("PCR0 commitment is bytes32(0)")]
InvalidPcr0Commitment,
#[error("newStateRoot is bytes32(0)")]
InvalidNewStateRoot,
#[error("unsupported StateCommit version: expected={expected}, got={got}")]
UnsupportedStateCommitVersion {
expected: u8,
got: u8,
},
#[error("sealed snapshot payload malformed or signature empty")]
InvalidSealedSnapshot,
#[error("certificate messageHash mismatch: expected={expected}, actual={actual}")]
CertificateMessageHashMismatch {
expected: B256,
actual: B256,
},
#[error("transaction submission timed out after {timeout_secs}s")]
ReceiptTimeout {
timeout_secs: u64,
},
#[error("transaction {tx_hash} reverted on-chain")]
TransactionReverted {
tx_hash: B256,
},
#[error("unknown revert: 0x{selector_hex}")]
UnknownRevert {
selector_hex: String,
},
#[error("on-chain call failed: {0}")]
OnchainCallFailed(#[source] ChainIoError),
#[error("quorum not reached: signed={signed_bps}bps required={required_bps}bps")]
QuorumNotReached {
signed_bps: u16,
required_bps: u16,
},
#[error("BLS aggregation failed: {0}")]
AggregationFailed(String),
#[error("PCR0 lookup failed: {0}")]
Pcr0Lookup(String),
#[error("clock skew detected: {0}")]
ClockSkew(String),
#[error("operator proposal disagreement at sequence {sequence_no}")]
OperatorDisagreement {
sequence_no: u64,
},
}
impl StateCommitError {
pub fn is_poison(&self) -> bool {
matches!(
self,
Self::SequenceGap { .. }
| Self::StateRootMismatch { .. }
| Self::TimestampRegression { .. }
| Self::InvalidPcr0Commitment
| Self::InvalidNewStateRoot
| Self::UnsupportedStateCommitVersion { .. }
| Self::InvalidSealedSnapshot
| Self::CertificateMessageHashMismatch { .. }
| Self::ReceiptTimeout { .. }
| Self::TransactionReverted { .. }
| Self::UnknownRevert { .. }
)
}
}
pub fn from_chainio(err: ChainIoError) -> StateCommitError {
match &err {
ChainIoError::TransactionTimeout { timeout_secs } => {
return StateCommitError::ReceiptTimeout {
timeout_secs: *timeout_secs,
};
}
ChainIoError::TransactionReverted(tx) => {
return StateCommitError::TransactionReverted { tx_hash: *tx };
}
ChainIoError::ContractError(ce) | ChainIoError::ContractErrorWithTx { source: ce, .. } => {
if let Some(data) = ce.as_revert_data() {
return decode_revert_data(&data);
}
}
_ => {}
}
StateCommitError::OnchainCallFailed(err)
}
pub(crate) fn decode_revert_data(data: &[u8]) -> StateCommitError {
if data.len() < 4 {
return StateCommitError::UnknownRevert {
selector_hex: alloy::hex::encode(data),
};
}
let selector: [u8; 4] = data[..4].try_into().expect("len >= 4 checked above");
if selector == SequenceGap::SELECTOR {
return SequenceGap::abi_decode(data)
.map(|e| StateCommitError::SequenceGap {
expected: e.expected,
got: e.got,
})
.unwrap_or_else(|_| unknown(&selector));
}
if selector == StateRootMismatch::SELECTOR {
return StateRootMismatch::abi_decode(data)
.map(|e| StateCommitError::StateRootMismatch {
expected: e.expected,
got: e.got,
})
.unwrap_or_else(|_| unknown(&selector));
}
if selector == TimestampRegression::SELECTOR {
return TimestampRegression::abi_decode(data)
.map(|e| StateCommitError::TimestampRegression {
last: e.last,
got: e.got,
})
.unwrap_or_else(|_| unknown(&selector));
}
if selector == InvalidPcr0Commitment::SELECTOR {
return StateCommitError::InvalidPcr0Commitment;
}
if selector == InvalidNewStateRoot::SELECTOR {
return StateCommitError::InvalidNewStateRoot;
}
if selector == UnsupportedStateCommitVersion::SELECTOR {
return UnsupportedStateCommitVersion::abi_decode(data)
.map(|e| StateCommitError::UnsupportedStateCommitVersion {
expected: e.expected,
got: e.got,
})
.unwrap_or_else(|_| unknown(&selector));
}
if selector == InvalidSealedSnapshot::SELECTOR {
return StateCommitError::InvalidSealedSnapshot;
}
if selector == CertificateMessageHashMismatch::SELECTOR {
return CertificateMessageHashMismatch::abi_decode(data)
.map(|e| StateCommitError::CertificateMessageHashMismatch {
expected: e.expected,
actual: e.actual,
})
.unwrap_or_else(|_| unknown(&selector));
}
unknown(&selector)
}
fn unknown(selector: &[u8; 4]) -> StateCommitError {
StateCommitError::UnknownRevert {
selector_hex: alloy::hex::encode(selector),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::FixedBytes;
fn b32(byte: u8) -> B256 {
B256::repeat_byte(byte)
}
fn fb32(byte: u8) -> FixedBytes<32> {
FixedBytes::repeat_byte(byte)
}
#[test]
fn is_poison_typed_reverts_all_true() {
let cases: Vec<StateCommitError> = vec![
StateCommitError::SequenceGap { expected: 5, got: 4 },
StateCommitError::StateRootMismatch {
expected: b32(0xaa),
got: b32(0xbb),
},
StateCommitError::TimestampRegression { last: 100, got: 100 },
StateCommitError::InvalidPcr0Commitment,
StateCommitError::InvalidNewStateRoot,
StateCommitError::UnsupportedStateCommitVersion { expected: 1, got: 2 },
StateCommitError::InvalidSealedSnapshot,
StateCommitError::CertificateMessageHashMismatch {
expected: b32(0xaa),
actual: b32(0xbb),
},
];
for case in cases {
assert!(case.is_poison(), "typed revert variant must be poison: {case:?}");
}
}
#[test]
fn is_poison_transport_failures_all_true() {
let cases: Vec<StateCommitError> = vec![
StateCommitError::ReceiptTimeout { timeout_secs: 60 },
StateCommitError::TransactionReverted { tx_hash: b32(0x11) },
StateCommitError::UnknownRevert {
selector_hex: "deadbeef".into(),
},
];
for case in cases {
assert!(case.is_poison(), "transport failure must be poison: {case:?}");
}
}
#[test]
fn is_poison_config_and_transient_false() {
assert!(!StateCommitError::RegistryNotConfigured.is_poison());
let inner = ChainIoError::SendAggregatedResponseError;
assert!(!StateCommitError::OnchainCallFailed(inner).is_poison());
}
#[test]
fn decode_sequence_gap_extracts_fields() {
let on_chain = SequenceGap { expected: 42, got: 41 };
let data = on_chain.abi_encode();
match decode_revert_data(&data) {
StateCommitError::SequenceGap { expected, got } => {
assert_eq!(expected, 42);
assert_eq!(got, 41);
}
other => panic!("expected SequenceGap, got {other:?}"),
}
}
#[test]
fn decode_state_root_mismatch_extracts_fields() {
let on_chain = StateRootMismatch {
expected: fb32(0xaa),
got: fb32(0xbb),
};
let data = on_chain.abi_encode();
match decode_revert_data(&data) {
StateCommitError::StateRootMismatch { expected, got } => {
assert_eq!(expected, b32(0xaa));
assert_eq!(got, b32(0xbb));
}
other => panic!("expected StateRootMismatch, got {other:?}"),
}
}
#[test]
fn decode_timestamp_regression_extracts_fields() {
let on_chain = TimestampRegression { last: 100, got: 100 };
let data = on_chain.abi_encode();
match decode_revert_data(&data) {
StateCommitError::TimestampRegression { last, got } => {
assert_eq!(last, 100);
assert_eq!(got, 100);
}
other => panic!("expected TimestampRegression, got {other:?}"),
}
}
#[test]
fn decode_invalid_pcr0_commitment() {
let data = InvalidPcr0Commitment {}.abi_encode();
assert!(matches!(
decode_revert_data(&data),
StateCommitError::InvalidPcr0Commitment
));
}
#[test]
fn decode_invalid_new_state_root() {
let data = InvalidNewStateRoot {}.abi_encode();
assert!(matches!(
decode_revert_data(&data),
StateCommitError::InvalidNewStateRoot
));
}
#[test]
fn decode_unsupported_version_extracts_fields() {
let on_chain = UnsupportedStateCommitVersion { expected: 1, got: 2 };
let data = on_chain.abi_encode();
match decode_revert_data(&data) {
StateCommitError::UnsupportedStateCommitVersion { expected, got } => {
assert_eq!(expected, 1);
assert_eq!(got, 2);
}
other => panic!("expected UnsupportedStateCommitVersion, got {other:?}"),
}
}
#[test]
fn decode_invalid_sealed_snapshot() {
let data = InvalidSealedSnapshot {}.abi_encode();
assert!(matches!(
decode_revert_data(&data),
StateCommitError::InvalidSealedSnapshot
));
}
#[test]
fn decode_cert_message_hash_mismatch_extracts_fields() {
let on_chain = CertificateMessageHashMismatch {
expected: fb32(0xcc),
actual: fb32(0xdd),
};
let data = on_chain.abi_encode();
match decode_revert_data(&data) {
StateCommitError::CertificateMessageHashMismatch { expected, actual } => {
assert_eq!(expected, b32(0xcc));
assert_eq!(actual, b32(0xdd));
}
other => panic!("expected CertificateMessageHashMismatch, got {other:?}"),
}
}
#[test]
fn decode_empty_data_returns_unknown() {
match decode_revert_data(&[]) {
StateCommitError::UnknownRevert { selector_hex } => assert!(selector_hex.is_empty()),
other => panic!("expected UnknownRevert, got {other:?}"),
}
}
#[test]
fn decode_short_data_returns_unknown_with_partial_hex() {
match decode_revert_data(&[0xab, 0xcd]) {
StateCommitError::UnknownRevert { selector_hex } => assert_eq!(selector_hex, "abcd"),
other => panic!("expected UnknownRevert, got {other:?}"),
}
}
#[test]
fn decode_unknown_selector_returns_unknown_with_hex() {
let data = [0xde, 0xad, 0xbe, 0xef];
match decode_revert_data(&data) {
StateCommitError::UnknownRevert { selector_hex } => assert_eq!(selector_hex, "deadbeef"),
other => panic!("expected UnknownRevert, got {other:?}"),
}
}
#[test]
fn from_chainio_timeout_maps_to_receipt_timeout() {
let err = ChainIoError::TransactionTimeout { timeout_secs: 60 };
match from_chainio(err) {
StateCommitError::ReceiptTimeout { timeout_secs } => assert_eq!(timeout_secs, 60),
other => panic!("expected ReceiptTimeout, got {other:?}"),
}
}
#[test]
fn from_chainio_reverted_maps_to_transaction_reverted() {
let tx = b32(0x42);
match from_chainio(ChainIoError::TransactionReverted(tx)) {
StateCommitError::TransactionReverted { tx_hash } => assert_eq!(tx_hash, tx),
other => panic!("expected TransactionReverted, got {other:?}"),
}
}
#[test]
fn from_chainio_non_revert_maps_to_onchain_call_failed() {
let err = ChainIoError::SendAggregatedResponseError;
assert!(matches!(from_chainio(err), StateCommitError::OnchainCallFailed(_)));
}
}