Skip to content

nonce_validation - [mainnet]

use 0x1::aptos_hash;
use 0x1::big_ordered_map;
use 0x1::error;
use 0x1::option;
use 0x1::system_addresses;
use 0x1::table;
use 0x1::timestamp;

Constants

const ETRANSACTION_EXPIRATION_TOO_FAR_IN_FUTURE: u64 = 1002;
const E_NONCE_HISTORY_DOES_NOT_EXIST: u64 = 1001;
const MAX_ENTRIES_GARBAGE_COLLECTED_PER_CALL: u64 = 5;
const NONCE_REPLAY_PROTECTION_OVERLAP_INTERVAL_SECS: u64 = 65;
const NUM_BUCKETS: u64 = 50000;

Structs

Bucket

struct Bucket has store
Fields
nonces_ordered_by_exp_time: big_ordered_map::BigOrderedMap<nonce_validation::NonceKeyWithExpTime, bool>
nonce_to_exp_time_map: big_ordered_map::BigOrderedMap<nonce_validation::NonceKey, u64>

NonceKeyWithExpTime

struct NonceKeyWithExpTime has copy, drop, store
Fields
txn_expiration_time: u64
sender_address: address
nonce: u64

NonceKey

struct NonceKey has copy, drop, store
Fields
sender_address: address
nonce: u64

Resources

NonceHistory

struct NonceHistory has key
Fields
nonce_table: table::Table<u64, nonce_validation::Bucket>
next_key: u64

Functions

initialize

public(friend) fun initialize(aptos_framework: &signer)
Implementation
public(friend) fun initialize(aptos_framework: &signer) {
initialize_nonce_table(aptos_framework);
}

initialize_nonce_table

public entry fun initialize_nonce_table(aptos_framework: &signer)
Implementation
public entry fun initialize_nonce_table(aptos_framework: &signer) {
system_addresses::assert_aptos_framework(aptos_framework);
if (!exists<NonceHistory>(@aptos_framework)) {
let table = table::new();
let nonce_history = NonceHistory {
nonce_table: table,
next_key: 0,
};
move_to<NonceHistory>(aptos_framework, nonce_history);
};
}

empty_bucket

fun empty_bucket(pre_allocate_slots: bool): nonce_validation::Bucket
Implementation
fun empty_bucket(pre_allocate_slots: bool): Bucket {
let bucket = Bucket {
nonces_ordered_by_exp_time: big_ordered_map::new_with_reusable(),
nonce_to_exp_time_map: big_ordered_map::new_with_reusable(),
};
if (pre_allocate_slots) {
// Initiating big ordered maps with 5 pre-allocated storage slots.
// (expiration time, address, nonce) is together 48 bytes.
// A 4 KB storage slot can store 80+ such tuples.
// The 5 slots should be more than enough for the current use case.
bucket.nonces_ordered_by_exp_time.allocate_spare_slots(5);
bucket.nonce_to_exp_time_map.allocate_spare_slots(5);
};
bucket
}

add_nonce_buckets

public entry fun add_nonce_buckets(count: u64)
Implementation
public entry fun add_nonce_buckets(count: u64) acquires NonceHistory {
assert!(exists<NonceHistory>(@aptos_framework), error::invalid_state(E_NONCE_HISTORY_DOES_NOT_EXIST));
let nonce_history = &mut NonceHistory[@aptos_framework];
for (i in 0..count) {
if (nonce_history.next_key <= NUM_BUCKETS) {
if (!nonce_history.nonce_table.contains(nonce_history.next_key)) {
nonce_history.nonce_table.add(
nonce_history.next_key,
empty_bucket(true)
);
};
nonce_history.next_key = nonce_history.next_key + 1;
}
}
}

check_and_insert_nonce

public(friend) fun check_and_insert_nonce(sender_address: address, nonce: u64, txn_expiration_time: u64): bool
Implementation
public(friend) fun check_and_insert_nonce(
sender_address: address,
nonce: u64,
txn_expiration_time: u64,
): bool acquires NonceHistory {
assert!(exists<NonceHistory>(@aptos_framework), error::invalid_state(E_NONCE_HISTORY_DOES_NOT_EXIST));
// Check if the transaction expiration time is too far in the future.
assert!(txn_expiration_time <= timestamp::now_seconds() + NONCE_REPLAY_PROTECTION_OVERLAP_INTERVAL_SECS, error::invalid_argument(ETRANSACTION_EXPIRATION_TOO_FAR_IN_FUTURE));
let nonce_history = &mut NonceHistory[@aptos_framework];
let nonce_key = NonceKey {
sender_address,
nonce,
};
let bucket_index = sip_hash_from_value(&nonce_key) % NUM_BUCKETS;
let current_time = timestamp::now_seconds();
if (!nonce_history.nonce_table.contains(bucket_index)) {
nonce_history.nonce_table.add(
bucket_index,
empty_bucket(false)
);
};
let bucket = table::borrow_mut(&mut nonce_history.nonce_table, bucket_index);
let existing_exp_time = bucket.nonce_to_exp_time_map.get(&nonce_key);
if (existing_exp_time.is_some()) {
let existing_exp_time = existing_exp_time.extract();
// If the existing (address, nonce) pair has not expired, return false.
if (existing_exp_time >= current_time) {
return false;
};
// We maintain an invariant that two transaction with the same (address, nonce) pair cannot be stored
// in the nonce history if their transaction expiration times are less than `NONCE_REPLAY_PROTECTION_OVERLAP_INTERVAL_SECS`
// seconds apart.
if (txn_expiration_time <= existing_exp_time + NONCE_REPLAY_PROTECTION_OVERLAP_INTERVAL_SECS) {
return false;
};
// If the existing (address, nonce) pair has expired, garbage collect it.
bucket.nonce_to_exp_time_map.remove(&nonce_key);
bucket.nonces_ordered_by_exp_time.remove(&NonceKeyWithExpTime {
txn_expiration_time: existing_exp_time,
sender_address,
nonce,
});
};
// Garbage collect upto MAX_ENTRIES_GARBAGE_COLLECTED_PER_CALL expired nonces in the bucket.
let i = 0;
while (i < MAX_ENTRIES_GARBAGE_COLLECTED_PER_CALL && !bucket.nonces_ordered_by_exp_time.is_empty()) {
let (front_k, _) = bucket.nonces_ordered_by_exp_time.borrow_front();
// We garbage collect a nonce after it has expired and the NONCE_REPLAY_PROTECTION_OVERLAP_INTERVAL_SECS
// seconds have passed.
if (front_k.txn_expiration_time + NONCE_REPLAY_PROTECTION_OVERLAP_INTERVAL_SECS < current_time) {
bucket.nonces_ordered_by_exp_time.pop_front();
bucket.nonce_to_exp_time_map.remove(&NonceKey {
sender_address: front_k.sender_address,
nonce: front_k.nonce,
});
} else {
break;
};
i = i + 1;
};
// Insert the (address, nonce) pair in the bucket.
let nonce_key_with_exp_time = NonceKeyWithExpTime {
txn_expiration_time,
sender_address,
nonce,
};
bucket.nonces_ordered_by_exp_time.add(nonce_key_with_exp_time, true);
bucket.nonce_to_exp_time_map.add(nonce_key, txn_expiration_time);
true
}