use {
crate::{
nexus::error::NexusError,
sui::{self, traits::*},
},
std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
LazyLock,
},
};
const NONCE_SPACE_SIZE: u64 = u32::MAX as u64 + 1;
#[derive(Debug)]
struct NonceSequence {
initial: u32,
issued: AtomicU64,
}
impl NonceSequence {
const fn new(initial: u32) -> Self {
Self {
initial,
issued: AtomicU64::new(0),
}
}
}
static PROCESS_NONCES: LazyLock<Arc<NonceSequence>> =
LazyLock::new(|| Arc::new(NonceSequence::new(rand::random())));
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SubmissionContext {
pub reference_gas_price: u64,
pub epoch: u64,
pub chain: sui::types::Digest,
}
#[derive(Clone, Debug)]
pub struct NonceAllocator {
sequence: Arc<NonceSequence>,
}
impl Default for NonceAllocator {
fn default() -> Self {
Self {
sequence: Arc::clone(&PROCESS_NONCES),
}
}
}
impl NonceAllocator {
pub fn new(initial: u32) -> Self {
Self {
sequence: Arc::new(NonceSequence::new(initial)),
}
}
#[cfg(test)]
fn with_issued(initial: u32, issued: u64) -> Self {
Self {
sequence: Arc::new(NonceSequence {
initial,
issued: AtomicU64::new(issued),
}),
}
}
pub fn allocate(&self) -> Result<u32, NexusError> {
let offset = self
.sequence
.issued
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
(current < NONCE_SPACE_SIZE).then_some(current + 1)
})
.map_err(|_| {
NexusError::Configuration(
"address balance transaction nonce space is exhausted".into(),
)
})?;
Ok(self.sequence.initial.wrapping_add(offset as u32))
}
}
pub async fn fetch_submission_context(
client: &mut sui::grpc::Client,
) -> Result<SubmissionContext, NexusError> {
let epoch_request =
sui::grpc::GetEpochRequest::latest().with_read_mask(sui::grpc::FieldMask::from_paths([
"epoch",
"reference_gas_price",
]));
let epoch_response = client
.ledger_client()
.get_epoch(epoch_request)
.await
.map_err(|error| NexusError::Rpc(error.into()))?
.into_inner();
let epoch = epoch_response.epoch();
let service_info = client
.ledger_client()
.get_service_info(sui::grpc::GetServiceInfoRequest::default())
.await
.map_err(|error| NexusError::Rpc(error.into()))?
.into_inner();
let chain = service_info
.chain_id
.ok_or_else(|| NexusError::Parsing(anyhow::anyhow!("service info is missing chain id")))?
.parse::<sui::types::Digest>()
.map_err(|error| NexusError::Parsing(error.into()))?;
Ok(SubmissionContext {
reference_gas_price: epoch.reference_gas_price(),
epoch: epoch.epoch(),
chain,
})
}
pub fn finish_transaction(
ptb: sui::types::ProgrammableTransaction,
sender: sui::types::Address,
gas_budget: u64,
context: SubmissionContext,
nonce: u32,
) -> sui::types::Transaction {
sui::types::Transaction {
kind: sui::types::TransactionKind::ProgrammableTransaction(ptb),
sender,
gas_payment: sui::types::GasPayment {
objects: Vec::new(),
owner: sender,
price: context.reference_gas_price,
budget: gas_budget,
},
expiration: sui::types::TransactionExpiration::ValidDuring {
min_epoch: Some(context.epoch),
max_epoch: Some(context.epoch.saturating_add(1)),
min_timestamp: None,
max_timestamp: None,
chain: context.chain,
nonce,
},
}
}
#[cfg(test)]
mod tests {
use {
super::*,
std::{collections::HashSet, thread},
};
#[test]
fn finishes_with_address_balance_gas_and_validity_context() {
let sender = sui::types::Address::from_static("0x42");
let chain = sui::types::Digest::from([7; 32]);
let context = SubmissionContext {
reference_gas_price: 1_234,
epoch: 17,
chain,
};
let ptb = sui::types::ProgrammableTransaction {
inputs: Vec::new(),
commands: Vec::new(),
};
let tx = finish_transaction(ptb, sender, 99_000, context, 41);
assert!(tx.gas_payment.objects.is_empty());
assert_eq!(tx.gas_payment.owner, sender);
assert_eq!(tx.gas_payment.price, 1_234);
assert_eq!(tx.gas_payment.budget, 99_000);
assert_eq!(
tx.expiration,
sui::types::TransactionExpiration::ValidDuring {
min_epoch: Some(17),
max_epoch: Some(18),
min_timestamp: None,
max_timestamp: None,
chain,
nonce: 41,
}
);
}
#[test]
fn cloned_allocators_issue_one_shared_nonce_sequence() {
let allocator = NonceAllocator::new(0);
let handles = (0..8)
.map(|_| {
let allocator = allocator.clone();
thread::spawn(move || {
(0..32)
.map(|_| allocator.allocate().unwrap())
.collect::<Vec<_>>()
})
})
.collect::<Vec<_>>();
let nonces = handles
.into_iter()
.flat_map(|handle| handle.join().unwrap())
.collect::<HashSet<_>>();
assert_eq!(nonces.len(), 256);
assert_eq!(nonces.iter().copied().min(), Some(0));
assert_eq!(nonces.iter().copied().max(), Some(255));
}
#[test]
fn independent_default_allocators_do_not_repeat_transaction_nonces() {
let first = NonceAllocator::default().allocate().unwrap();
let second = NonceAllocator::default().allocate().unwrap();
assert_ne!(first, second);
}
#[test]
fn nonce_allocator_rejects_exhaustion() {
let allocator = NonceAllocator::with_issued(7, u32::MAX as u64);
assert_eq!(allocator.allocate().unwrap(), 6);
let error = allocator.allocate().unwrap_err();
assert!(error.to_string().contains("nonce space is exhausted"));
}
}