Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,43 @@ module aptos_experimental::confidential_balance {

ok
}

//
// Fuzz tests
//

// `split_into_chunks_u64`/`_u128` decompose an integer into 16-bit limbs and
// lift each limb to a Ristretto `Scalar`. The shift/mask logic
// (`amount >> (i * CHUNK_SIZE_BITS) & 0xffff`) is exactly where an off-by-one
// in the chunk count, the bit width, or the limb ordering slips past
// hand-picked examples. We fuzz the full integer range and check each emitted
// scalar against an independently computed little-endian limb decomposition;
// the assertion only holds if every chunk is the correct 16 bits in the
// correct position, and it covers the high limbs that fixed examples rarely
// reach.
#[test]
fun fuzz_split_into_chunks_u64(amount: u64) {
let chunks = split_into_chunks_u64(amount);
assert!(vector::length(&chunks) == PENDING_BALANCE_CHUNKS, 0);
let i = 0;
while (i < PENDING_BALANCE_CHUNKS) {
let expected = (amount >> ((i * CHUNK_SIZE_BITS) as u8)) & 0xffff;
let expected_scalar = ristretto255::new_scalar_from_u64(expected);
assert!(ristretto255::scalar_equals(vector::borrow(&chunks, i), &expected_scalar), 1);
i = i + 1;
};
}

#[test]
fun fuzz_split_into_chunks_u128(amount: u128) {
let chunks = split_into_chunks_u128(amount);
assert!(vector::length(&chunks) == ACTUAL_BALANCE_CHUNKS, 0);
let i = 0;
while (i < ACTUAL_BALANCE_CHUNKS) {
let expected = (amount >> ((i * CHUNK_SIZE_BITS) as u8)) & 0xffff;
let expected_scalar = ristretto255::new_scalar_from_u128(expected);
assert!(ristretto255::scalar_equals(vector::borrow(&chunks, i), &expected_scalar), 1);
i = i + 1;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,67 @@ module aptos_framework::rate_limiter {
refill(&mut bucket);
assert!(bucket.current_amount == 10, 603); // Should be full again
}

//
// Fuzz tests
//

// A token bucket must never hold more than its capacity, no matter how
// requests and time-based refills interleave. `refill`'s arithmetic is where
// a fuzzer earns its keep: a zero `refill_interval` divides by zero, and an
// unbounded `capacity`/elapsed pair overflows `time_passed * capacity` or the
// running `current_amount + new_tokens`. We bound the drawn values into
// realistic ranges so the test exercises the logic rather than tripping the
// u64 guardrails, and assert the capacity invariant plus exact accounting on
// the no-elapsed-time path.
#[test(aptos_framework = @0x1)]
fun fuzz_request_respects_capacity(
aptos_framework: &signer,
cap_raw: u64,
interval_raw: u64,
req_raw: u64,
) {
timestamp::set_time_has_started_for_testing(aptos_framework);
let capacity = 1 + cap_raw % 1000000; // [1, 1_000_000]
let interval = 1 + interval_raw % 86400; // [1, 86400]; never 0
let bucket = initialize(capacity, interval);

// No time has elapsed since initialize(), so the refill inside request()
// adds nothing: a full bucket grants exactly `req` when req <= capacity.
let req = req_raw % (capacity + 1); // [0, capacity]
let granted = request(&mut bucket, req);
assert!(granted, 0);
assert!(bucket.current_amount == capacity - req, 1);
assert!(bucket.current_amount <= bucket.capacity, 2);
}

#[test(aptos_framework = @0x1)]
fun fuzz_refill_never_exceeds_capacity(
aptos_framework: &signer,
cap_raw: u64,
interval_raw: u64,
elapsed_raw: u64,
) {
timestamp::set_time_has_started_for_testing(aptos_framework);
let capacity = 1 + cap_raw % 1000000; // [1, 1_000_000]
let interval = 1 + interval_raw % 86400; // [1, 86400]
let bucket = initialize(capacity, interval);

// Drain fully, advance the clock by a bounded, strictly positive amount
// (update_global_time_for_test requires time to move forward), then
// refill. With capacity <= 1e6 and elapsed <= 1e5, `time_passed *
// capacity` <= 1e11 — well inside u64 — so we test the refill logic, not
// overflow.
assert!(request(&mut bucket, capacity), 0);
assert!(bucket.current_amount == 0, 1);

let elapsed = 1 + elapsed_raw % 100000; // [1, 100_000] seconds
timestamp::update_global_time_for_test_secs(timestamp::now_seconds() + elapsed);
refill(&mut bucket);

// A refill tops up to, but never beyond, capacity...
assert!(bucket.current_amount <= bucket.capacity, 2);
// ...and any carried-over fractional accumulation is a proper remainder.
assert!(bucket.fractional_accumulated < interval, 3);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,54 @@ module aptos_framework::optional_aggregator {

destroy(aggregator);
}

//
// Fuzz tests
//

// `add_integer` aborts unless `value <= limit - current`, and `sub_integer`
// aborts unless `value <= current`. The bug classes worth hunting are an
// off-by-one at the limit boundary and an underflow in the `limit - value`
// headroom computation. We fuzz the limit and both operands independently
// and fold each operand into the always-legal range inside the body, then
// assert the stored value tracks the adds and subs exactly and never exceeds
// the limit. The fold keeps every drawn case on the success path, so the
// assertions — not aborts — are what catch a regression.
#[test]
fun fuzz_integer_add_sub_tracks_value(limit: u128, add_raw: u128, sub_raw: u128) {
let integer = new_integer(limit);
assert!(read_integer(&integer) == 0, 0);

// `v` in [0, limit]. `limit + 1` would overflow only at limit == MAX_U128,
// where every u128 already satisfies v <= limit.
let v = if (limit == MAX_U128) { add_raw } else { add_raw % (limit + 1) };
add_integer(&mut integer, v);
assert!(read_integer(&integer) == v, 1);
assert!(read_integer(&integer) <= limit, 2);

// `s` in [0, v]; same overflow guard against v == MAX_U128.
let s = if (v == MAX_U128) { sub_raw } else { sub_raw % (v + 1) };
sub_integer(&mut integer, s);
assert!(read_integer(&integer) == v - s, 3);

destroy_integer(integer);
}

// Same accounting property exercised end-to-end through the public
// `add`/`sub`/`read` surface on the integer-backed aggregator, whose limit is
// MAX_U128 (so any single u128 operand fits). This covers the option dispatch
// in `add`/`sub`/`read` that the internal test above bypasses.
#[test(account = @aptos_framework)]
fun fuzz_optional_aggregator_roundtrip(account: signer, a: u128, b: u128) {
aggregator_factory::initialize_aggregator_factory(&account);
let aggregator = new(false);
add(&mut aggregator, a);
assert!(read(&aggregator) == a, 0);

let s = if (a == MAX_U128) { b } else { b % (a + 1) };
sub(&mut aggregator, s);
assert!(read(&aggregator) == a - s, 1);

destroy(aggregator);
}
}
40 changes: 40 additions & 0 deletions aptos-move/framework/aptos-framework/sources/storage_gas.move
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,44 @@ module aptos_framework::storage_gas {
assert!(gas_parameter.per_byte_read == 1000, 0);
};
}

//
// Fuzz tests
//

// `interpolate` is integer linear interpolation:
// y0 + (x - x0) * (y1 - y0) / (x1 - x0)
// It is only well-defined when x0 < x1 (else division by zero), x0 <= x <= x1
// and y0 <= y1 (else u64 underflow in the subtractions), and when the
// intermediate product stays within u64. Fuzz constraints are applied per
// parameter and cannot express these cross-parameter relationships, so we
// draw five unconstrained u64s and fold them into a valid configuration in
// the body. The property under test is the defining one for linear
// interpolation: the output never leaves the [y0, y1] band, and the
// endpoints map exactly.
#[test]
fun fuzz_interpolate_stays_within_bounds(
x0_raw: u64,
span_raw: u64,
frac_raw: u64,
y0_raw: u64,
dy_raw: u64,
) {
// Keep the x-coordinates in basis-point territory (the only domain
// `interpolate` is ever called with) and the y-values well below the
// overflow threshold: (x - x0) <= 10_000 and (y1 - y0) < 1_000_000_000, so
// the product is at most ~1e13, comfortably inside u64.
let x0 = x0_raw % 10000;
let x1 = x0 + 1 + (span_raw % 10000); // x1 in [x0 + 1, x0 + 10000]
let x = x0 + (frac_raw % (x1 - x0 + 1)); // x in [x0, x1]
let y0 = y0_raw % 1000000000;
let y1 = y0 + (dy_raw % 1000000000); // y1 in [y0, y0 + 1e9 - 1]

let y = interpolate(x0, x1, y0, y1, x);
assert!(y >= y0, 0);
assert!(y <= y1, 1);
// Endpoints are exact: at x0 the offset is 0, at x1 it is the full (y1 - y0).
assert!(interpolate(x0, x1, y0, y1, x0) == y0, 2);
assert!(interpolate(x0, x1, y0, y1, x1) == y1, 3);
}
}
1 change: 1 addition & 0 deletions third_party/move/move-compiler-v2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ move-symbol-pool = { workspace = true }
num = { workspace = true }
once_cell = { workspace = true }
petgraph = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

[dev-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,28 @@ pub enum AttributeValue_ {
Value(Value),
Module(ModuleIdent),
ModuleAccess(ModuleAccess),
List(Vec<AttributeValue>),
Range {
lo: Box<AttributeValue>,
hi: Box<AttributeValue>,
inclusive_hi: bool,
},
Union(Vec<AttributeValue>),
}
pub type AttributeValue = Spanned<AttributeValue_>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintOp {
Ne,
In,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Attribute_ {
Name(Name),
Assigned(Name, Box<AttributeValue>),
Parameterized(Name, Attributes),
Constrained(Name, ConstraintOp, Box<AttributeValue>),
}
pub type Attribute = Spanned<Attribute_>;

Expand All @@ -61,7 +75,8 @@ impl Attribute_ {
match self {
Attribute_::Name(nm)
| Attribute_::Assigned(nm, _)
| Attribute_::Parameterized(nm, _) => nm,
| Attribute_::Parameterized(nm, _)
| Attribute_::Constrained(nm, _, _) => nm,
}
}
}
Expand All @@ -70,6 +85,11 @@ impl Attribute_ {
pub enum AttributeName_ {
Unknown(Symbol),
Known(KnownAttribute),
/// Unique-by-construction key used by `Attribute_::Constrained` entries.
/// `slot` makes duplicate constraints on the same parameter (e.g.
/// `a in X, a != Y`) distinct in the [`UniqueMap`] that stores attributes,
/// while [`Display`](std::fmt::Display) still shows the underlying name.
Disambiguated(Symbol, u32),
}
pub type AttributeName = Spanned<AttributeName_>;

Expand Down Expand Up @@ -880,6 +900,7 @@ impl fmt::Display for AttributeName_ {
match self {
AttributeName_::Unknown(sym) => write!(f, "{}", sym),
AttributeName_::Known(known) => write!(f, "{}", known.name()),
AttributeName_::Disambiguated(sym, _) => write!(f, "{}", sym),
}
}
}
Expand Down Expand Up @@ -990,6 +1011,29 @@ impl AstDebug for AttributeValue_ {
AttributeValue_::Value(v) => v.ast_debug(w),
AttributeValue_::Module(m) => w.write(&format!("{}", m)),
AttributeValue_::ModuleAccess(n) => n.ast_debug(w),
AttributeValue_::List(items) => {
w.write("[");
w.list(items, ", ", |w, item| {
item.value.ast_debug(w);
false
});
w.write("]");
},
AttributeValue_::Range {
lo,
hi,
inclusive_hi,
} => {
lo.value.ast_debug(w);
w.write(if *inclusive_hi { "..=" } else { ".." });
hi.value.ast_debug(w);
},
AttributeValue_::Union(items) => {
w.list(items, " | ", |w, item| {
item.value.ast_debug(w);
false
});
},
}
}
}
Expand All @@ -1012,6 +1056,14 @@ impl AstDebug for Attribute_ {
});
w.write(")");
},
Attribute_::Constrained(n, op, v) => {
w.write(&format!("{}", n));
w.write(match op {
ConstraintOp::Ne => " != ",
ConstraintOp::In => " in ",
});
v.ast_debug(w);
},
}
}
}
Expand Down
Loading
Loading