Skip to content
Closed
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
31 changes: 31 additions & 0 deletions benchmarks/results/commitment_core_created_between_bucket_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# commitment_core created_between bucket index

## Scope

`commitment_core::get_commitments_created_between(from_ts, to_ts)` previously loaded
`DataKey::AllCommitmentIds` and read every commitment before filtering by `created_at`.

This change adds a UTC-day creation index:

- `DataKey::CommitmentCreatedBucketDays`
- `DataKey::CommitmentsCreatedInBucket(day)`

`create_commitment` appends each new commitment ID to its creation-day bucket, and
`get_commitments_created_between` reads only bucket IDs for non-empty days whose bucket
falls inside the requested timestamp range.

## Cost shape

Before:

- Storage reads: `AllCommitmentIds` plus one commitment read for every commitment ever created.
- Query cost: `O(total_commitments)`.

After:

- Storage reads: `CommitmentCreatedBucketDays`, relevant bucket vectors, and commitment records
inside those relevant buckets.
- Query cost: `O(non_empty_bucket_days + commitments_in_matching_buckets)`.

The new tests cover empty ranges, reversed ranges, single-bucket boundaries, multi-bucket
queries, and equivalence with the old full-scan filter order.
66 changes: 60 additions & 6 deletions contracts/commitment_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub mod fuzzing;

/// Maximum page size for paginated owner-commitment queries.
const MAX_PAGE_SIZE: u32 = 50;
/// Commitment creation timestamps are indexed into UTC-day buckets.
const COMMITMENT_CREATED_BUCKET_SECONDS: u64 = 86_400;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
Expand Down Expand Up @@ -183,6 +185,10 @@ pub enum DataKey {
AuthorizedOperator(Address),
/// All commitment IDs for time-range queries (analytics). Appended on create.
AllCommitmentIds,
/// Non-empty creation-day buckets. Used to avoid scanning every commitment for range queries.
CommitmentCreatedBucketDays,
/// Commitment IDs created during a specific UTC-day bucket.
CommitmentsCreatedInBucket(u64),
/// Fee recipient (protocol treasury) for fee withdrawals
FeeRecipient,
/// Creation fee rate in basis points (0-10000)
Expand Down Expand Up @@ -220,6 +226,35 @@ fn transfer_assets(e: &Env, from: &Address, to: &Address, asset_address: &Addres
token_client.transfer(from, to, &amount);
}

fn created_bucket_day(created_at: u64) -> u64 {
created_at / COMMITMENT_CREATED_BUCKET_SECONDS
}

fn add_commitment_to_created_bucket(e: &Env, commitment: &Commitment) {
let bucket_day = created_bucket_day(commitment.created_at);
let bucket_key = DataKey::CommitmentsCreatedInBucket(bucket_day);
let mut bucket_ids = e
.storage()
.instance()
.get::<_, Vec<String>>(&bucket_key)
.unwrap_or(Vec::new(e));

if bucket_ids.is_empty() {
let mut bucket_days = e
.storage()
.instance()
.get::<_, Vec<u64>>(&DataKey::CommitmentCreatedBucketDays)
.unwrap_or(Vec::new(e));
bucket_days.push_back(bucket_day);
e.storage()
.instance()
.set(&DataKey::CommitmentCreatedBucketDays, &bucket_days);
}

bucket_ids.push_back(commitment.commitment_id.clone());
e.storage().instance().set(&bucket_key, &bucket_ids);
}

/// Helper function to call NFT contract mint function.
fn call_nft_mint(
e: &Env,
Expand Down Expand Up @@ -596,6 +631,7 @@ impl CommitmentCoreContract {
e.storage()
.instance()
.set(&DataKey::AllCommitmentIds, &all_ids);
add_commitment_to_created_bucket(&e, &commitment);

let contract_address = e.current_contract_address();
transfer_assets(&e, &owner, &contract_address, &asset_address, amount);
Expand Down Expand Up @@ -712,16 +748,34 @@ impl CommitmentCoreContract {

/// Get commitment IDs created between two timestamps (inclusive).
pub fn get_commitments_created_between(e: Env, from_ts: u64, to_ts: u64) -> Vec<String> {
let all_ids = e
if from_ts > to_ts {
return Vec::new(&e);
}

let from_bucket = created_bucket_day(from_ts);
let to_bucket = created_bucket_day(to_ts);
let bucket_days = e
.storage()
.instance()
.get::<_, Vec<String>>(&DataKey::AllCommitmentIds)
.get::<_, Vec<u64>>(&DataKey::CommitmentCreatedBucketDays)
.unwrap_or(Vec::new(&e));
let mut out = Vec::new(&e);
for id in all_ids.iter() {
if let Some(c) = read_commitment(&e, &id) {
if c.created_at >= from_ts && c.created_at <= to_ts {
out.push_back(id.clone());

for bucket_day in bucket_days.iter() {
if bucket_day < from_bucket || bucket_day > to_bucket {
continue;
}

let bucket_ids = e
.storage()
.instance()
.get::<_, Vec<String>>(&DataKey::CommitmentsCreatedInBucket(bucket_day))
.unwrap_or(Vec::new(&e));
for id in bucket_ids.iter() {
if let Some(c) = read_commitment(&e, &id) {
if c.created_at >= from_ts && c.created_at <= to_ts {
out.push_back(id.clone());
}
}
}
}
Expand Down
151 changes: 150 additions & 1 deletion contracts/commitment_core/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,48 @@ fn store_commitment(e: &Env, contract_id: &Address, commitment: &Commitment) {
});
}

fn store_commitment_with_created_indexes(e: &Env, contract_id: &Address, commitment: &Commitment) {
e.as_contract(contract_id, || {
set_commitment(e, commitment);

let mut all_ids = e
.storage()
.instance()
.get::<_, Vec<String>>(&DataKey::AllCommitmentIds)
.unwrap_or(Vec::new(e));
all_ids.push_back(commitment.commitment_id.clone());
e.storage()
.instance()
.set(&DataKey::AllCommitmentIds, &all_ids);

add_commitment_to_created_bucket(e, commitment);
});
}

fn full_scan_commitments_created_between(
e: &Env,
contract_id: &Address,
from_ts: u64,
to_ts: u64,
) -> Vec<String> {
e.as_contract(contract_id, || {
let all_ids = e
.storage()
.instance()
.get::<_, Vec<String>>(&DataKey::AllCommitmentIds)
.unwrap_or(Vec::new(e));
let mut out = Vec::new(e);
for id in all_ids.iter() {
if let Some(c) = read_commitment(e, &id) {
if c.created_at >= from_ts && c.created_at <= to_ts {
out.push_back(id.clone());
}
}
}
out
})
}

#[test]
fn test_create_commitment_passes_core_contract_as_nft_caller() {
let e = Env::default();
Expand Down Expand Up @@ -1198,8 +1240,21 @@ fn test_create_commitment_updates_storage_layout() {
.get::<_, Vec<String>>(&DataKey::AllCommitmentIds)
.unwrap()
});
let bucket_day = created_bucket_day(e.ledger().timestamp());
let bucket_days = e.as_contract(&contract_id, || {
e.storage()
.instance()
.get::<_, Vec<u64>>(&DataKey::CommitmentCreatedBucketDays)
.unwrap()
});
let bucket_ids = e.as_contract(&contract_id, || {
e.storage()
.instance()
.get::<_, Vec<String>>(&DataKey::CommitmentsCreatedInBucket(bucket_day))
.unwrap()
});

assert_eq!(created_id, String::from_str(&e, "c_0"));
assert_eq!(created_id, String::from_str(&e, "COMMIT_0"));
assert_eq!(commitment.commitment_id, created_id);
assert_eq!(commitment.owner, owner);
assert_eq!(commitment.asset_address, asset_address.clone());
Expand All @@ -1216,11 +1271,105 @@ fn test_create_commitment_updates_storage_layout() {
assert_eq!(total_commitments, 1);
assert_eq!(total_value_locked, amount);
assert_eq!(all_ids, vec![&e, created_id.clone()]);
assert_eq!(bucket_days, vec![&e, bucket_day]);
assert_eq!(bucket_ids, vec![&e, created_id.clone()]);
assert_eq!(client.get_collected_fees(&asset_address), 0);
assert_eq!(token_client.balance(&owner), amount);
assert_eq!(token_client.balance(&contract_id), amount);
}

#[test]
fn test_get_commitments_created_between_uses_empty_bucket_index() {
let e = Env::default();
let contract_id = e.register_contract(None, CommitmentCoreContract);
let client = CommitmentCoreContractClient::new(&e, &contract_id);

assert_eq!(client.get_commitments_created_between(&0, &u64::MAX).len(), 0);
assert_eq!(client.get_commitments_created_between(&100, &99).len(), 0);
}

#[test]
fn test_get_commitments_created_between_reads_single_bucket_boundaries() {
let e = Env::default();
let contract_id = e.register_contract(None, CommitmentCoreContract);
let owner = Address::generate(&e);
let client = CommitmentCoreContractClient::new(&e, &contract_id);

let start = 2 * COMMITMENT_CREATED_BUCKET_SECONDS;
let first = create_test_commitment(&e, "first", &owner, 1000, 1000, 10, 30, start);
let middle = create_test_commitment(&e, "middle", &owner, 1000, 1000, 10, 30, start + 10);
let last = create_test_commitment(
&e,
"last",
&owner,
1000,
1000,
10,
30,
start + COMMITMENT_CREATED_BUCKET_SECONDS - 1,
);
store_commitment_with_created_indexes(&e, &contract_id, &first);
store_commitment_with_created_indexes(&e, &contract_id, &middle);
store_commitment_with_created_indexes(&e, &contract_id, &last);

assert_eq!(
client.get_commitments_created_between(
&start,
&(start + COMMITMENT_CREATED_BUCKET_SECONDS - 1)
),
vec![&e, first.commitment_id, middle.commitment_id, last.commitment_id]
);
}

#[test]
fn test_get_commitments_created_between_reads_multiple_relevant_buckets() {
let e = Env::default();
let contract_id = e.register_contract(None, CommitmentCoreContract);
let owner = Address::generate(&e);
let client = CommitmentCoreContractClient::new(&e, &contract_id);

let day = COMMITMENT_CREATED_BUCKET_SECONDS;
let before = create_test_commitment(&e, "before", &owner, 1000, 1000, 10, 30, day - 1);
let day_one = create_test_commitment(&e, "day_one", &owner, 1000, 1000, 10, 30, day);
let day_two = create_test_commitment(&e, "day_two", &owner, 1000, 1000, 10, 30, day * 2 + 5);
let after = create_test_commitment(&e, "after", &owner, 1000, 1000, 10, 30, day * 3);
store_commitment_with_created_indexes(&e, &contract_id, &before);
store_commitment_with_created_indexes(&e, &contract_id, &day_one);
store_commitment_with_created_indexes(&e, &contract_id, &day_two);
store_commitment_with_created_indexes(&e, &contract_id, &after);

assert_eq!(
client.get_commitments_created_between(&day, &(day * 2 + 5)),
vec![&e, day_one.commitment_id, day_two.commitment_id]
);
}

#[test]
fn test_get_commitments_created_between_matches_full_scan_order() {
let e = Env::default();
let contract_id = e.register_contract(None, CommitmentCoreContract);
let owner = Address::generate(&e);
let client = CommitmentCoreContractClient::new(&e, &contract_id);

for (id, created_at) in [
("c0", 10),
("c1", COMMITMENT_CREATED_BUCKET_SECONDS + 1),
("c2", COMMITMENT_CREATED_BUCKET_SECONDS * 2),
("c3", COMMITMENT_CREATED_BUCKET_SECONDS * 2 + 50),
("c4", COMMITMENT_CREATED_BUCKET_SECONDS * 4),
] {
let commitment = create_test_commitment(&e, id, &owner, 1000, 1000, 10, 30, created_at);
store_commitment_with_created_indexes(&e, &contract_id, &commitment);
}

let from_ts = COMMITMENT_CREATED_BUCKET_SECONDS;
let to_ts = COMMITMENT_CREATED_BUCKET_SECONDS * 3;
let expected = full_scan_commitments_created_between(&e, &contract_id, from_ts, to_ts);
let indexed = client.get_commitments_created_between(&from_ts, &to_ts);

assert_eq!(indexed, expected);
}

#[test]
fn test_create_commitment_validation_failures_do_not_mutate_storage() {
let e = Env::default();
Expand Down
Loading