From 5d9a86c063338c4a9f567254b389c5e8ae272a22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 17:51:33 +0000 Subject: [PATCH 1/3] Fix TokenStore deposit/withdraw fallback PV collision Token V1 module-event maps used for TokenStore-not-rewritten owner fallback were keyed only by token_data_id. Two property versions of the same named token in one txn overwrote each other, so the current ownership upsert used the wrong owner and the real row was skipped. Key and look up by (token_data_id, property_version). Co-authored-by: Young Yang Liauw --- .../token_v2/token_models/tokens.rs | 7 +- .../token_v2_models/v2_token_activities.rs | 16 +- .../token_v2_models/v2_token_ownerships.rs | 212 +++++++++++++++++- 3 files changed, 229 insertions(+), 6 deletions(-) diff --git a/processor/src/processors/token_v2/token_models/tokens.rs b/processor/src/processors/token_v2/token_models/tokens.rs index 0f462325..dbe05391 100644 --- a/processor/src/processors/token_v2/token_models/tokens.rs +++ b/processor/src/processors/token_v2/token_models/tokens.rs @@ -39,8 +39,11 @@ pub type CurrentTokenPendingClaimPK = (TokenDataIdHash, BigDecimal, Address, Add // PK of tokens table, used to dedupe tokens pub type TokenPK = (TokenDataIdHash, BigDecimal); // Map to keep track of token withdraw and deposit module events for token v1. -pub type TokenV1WithdrawModuleEvents = AHashMap; -pub type TokenV1DepositModuleEvents = AHashMap; +// Keyed by (token_data_id, property_version) so two PVs of the same named token +// in one txn do not share an owner fallback (TokenStore not rewritten). +pub type TokenV1ModuleEventPK = (TokenDataIdHash, BigDecimal); +pub type TokenV1WithdrawModuleEvents = AHashMap; +pub type TokenV1DepositModuleEvents = AHashMap; #[derive(Clone, Debug, Deserialize, FieldCount, Identifiable, Insertable, Serialize)] #[diesel(primary_key(token_data_id_hash, property_version, transaction_version))] diff --git a/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs b/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs index 5ea0d503..e293421a 100644 --- a/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs +++ b/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs @@ -279,7 +279,13 @@ impl TokenActivityV2 { to_address: None, token_amount: inner.amount.clone(), }; - tokens_withdrawn.insert(token_data_id_struct.to_id(), helper.clone()); + tokens_withdrawn.insert( + ( + token_data_id_struct.to_id(), + inner.id.property_version.clone(), + ), + helper.clone(), + ); helper }, TokenEvent::DepositTokenEvent(inner) => TokenActivityHelperV1 { @@ -298,7 +304,13 @@ impl TokenActivityV2 { to_address: Some(inner.get_account()), token_amount: inner.amount.clone(), }; - tokens_deposited.insert(token_data_id_struct.to_id(), helper.clone()); + tokens_deposited.insert( + ( + token_data_id_struct.to_id(), + inner.id.property_version.clone(), + ), + helper.clone(), + ); helper }, TokenEvent::OfferTokenEvent(inner) => TokenActivityHelperV1 { diff --git a/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs b/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs index a95159c4..7c0b3282 100644 --- a/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs +++ b/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs @@ -492,7 +492,10 @@ impl TokenOwnershipV2 { // If table_handle_to_owner doesn’t have the table metadata, it might be because // that module events were emitted, which means the resource won’t appear in the transaction. // Try getting the token metadata from the deposit module event instead. - let maybe_token_metadata = tokens_deposited.get(&token_data_id); + let maybe_token_metadata = tokens_deposited.get(&( + token_data_id.clone(), + token_id_struct.property_version.clone(), + )); let owner_address = if let Some(token_metadata) = maybe_token_metadata { token_metadata.to_address.clone().unwrap() } else { @@ -572,7 +575,10 @@ impl TokenOwnershipV2 { let owner_address = tm.get_owner_address(); (owner_address, Some(tm.table_type.clone())) } else { - let maybe_token_metadata = tokens_withdrawn.get(&token_data_id); + let maybe_token_metadata = tokens_withdrawn.get(&( + token_data_id.clone(), + token_id_struct.property_version.clone(), + )); let owner_address = if let Some(token_metadata) = maybe_token_metadata { token_metadata.from_address.clone().unwrap() } else { @@ -835,3 +841,205 @@ impl From for PostgresCurrentTokenOwnershipV2 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::processors::token_v2::{ + token_models::token_claims::{TokenV1Canceled, TokenV1Claimed}, + token_v2_models::v2_token_activities::TokenActivityV2, + }; + use aptos_indexer_processor_sdk::aptos_protos::transaction::v1::{ + delete_table_item, write_table_item, DeleteTableItem, Event, EventKey, WriteTableItem, + }; + + const CREATOR: &str = "0x1"; + const COLLECTION: &str = "C"; + const NAME: &str = "T"; + const ALICE: &str = "0xa11ce"; + const BOB: &str = "0xb0b"; + + fn ts() -> chrono::NaiveDateTime { + chrono::NaiveDateTime::default() + } + + fn token_id_json(property_version: &str) -> String { + format!( + r#"{{"token_data_id":{{"creator":"{CREATOR}","collection":"{COLLECTION}","name":"{NAME}"}},"property_version":"{property_version}"}}"# + ) + } + + fn token_deposit_event(account: &str, property_version: &str) -> Event { + Event { + key: Some(EventKey { + creation_number: 0, + account_address: "0x0".to_string(), + }), + sequence_number: 0, + type_str: "0x3::token::TokenDeposit".to_string(), + data: format!( + r#"{{"account":"{account}","amount":"1","id":{}}}"#, + token_id_json(property_version) + ), + ..Default::default() + } + } + + fn token_withdraw_event(account: &str, property_version: &str) -> Event { + Event { + key: Some(EventKey { + creation_number: 0, + account_address: "0x0".to_string(), + }), + sequence_number: 0, + type_str: "0x3::token::TokenWithdraw".to_string(), + data: format!( + r#"{{"account":"{account}","amount":"1","id":{}}}"#, + token_id_json(property_version) + ), + ..Default::default() + } + } + + fn write_token_table_item(handle: &str, property_version: &str) -> WriteTableItem { + let token_json = format!( + r#"{{"amount":"1","id":{},"token_properties":"0x00"}}"#, + token_id_json(property_version) + ); + WriteTableItem { + handle: handle.to_string(), + key: token_id_json(property_version), + data: Some(write_table_item::Data { + key: token_id_json(property_version), + key_type: "0x3::token::TokenId".to_string(), + value: token_json, + value_type: "0x3::token::Token".to_string(), + }), + ..Default::default() + } + } + + fn delete_token_table_item(handle: &str, property_version: &str) -> DeleteTableItem { + DeleteTableItem { + handle: handle.to_string(), + key: token_id_json(property_version), + data: Some(delete_table_item::Data { + key: token_id_json(property_version), + key_type: "0x3::token::TokenId".to_string(), + value: String::new(), + value_type: String::new(), + }), + ..Default::default() + } + } + + fn ingest_v1_events( + events: &[Event], + ) -> (TokenV1DepositModuleEvents, TokenV1WithdrawModuleEvents) { + let mut tokens_claimed: TokenV1Claimed = AHashMap::new(); + let mut tokens_canceled: TokenV1Canceled = AHashMap::new(); + let mut tokens_withdrawn = AHashMap::new(); + let mut tokens_deposited = AHashMap::new(); + for (i, event) in events.iter().enumerate() { + TokenActivityV2::get_v1_from_parsed_event( + event, + 1, + ts(), + i as i64, + &None, + &mut tokens_claimed, + &mut tokens_canceled, + &mut tokens_withdrawn, + &mut tokens_deposited, + ) + .unwrap(); + } + (tokens_deposited, tokens_withdrawn) + } + + #[test] + fn deposit_fallback_does_not_reuse_owner_across_property_versions() { + let (tokens_deposited, _) = ingest_v1_events(&[ + token_deposit_event(ALICE, "0"), + token_deposit_event(BOB, "1"), + ]); + let empty_owners = AHashMap::new(); + + let alice_row = TokenOwnershipV2::get_v1_from_write_table_item( + &write_token_table_item("0xaaa", "0"), + 1, + 0, + ts(), + &empty_owners, + &tokens_deposited, + ) + .unwrap() + .expect("pv0 deposit fallback") + .1 + .expect("current ownership"); + let bob_row = TokenOwnershipV2::get_v1_from_write_table_item( + &write_token_table_item("0xbbb", "1"), + 1, + 1, + ts(), + &empty_owners, + &tokens_deposited, + ) + .unwrap() + .expect("pv1 deposit fallback") + .1 + .expect("current ownership"); + + assert_eq!( + alice_row.owner_address, + standardize_address(ALICE), + "pv0 must keep Alice, not the later Bob deposit" + ); + assert_eq!(bob_row.owner_address, standardize_address(BOB)); + assert_eq!(alice_row.property_version_v1, BigDecimal::from(0)); + assert_eq!(bob_row.property_version_v1, BigDecimal::from(1)); + } + + #[test] + fn withdraw_fallback_does_not_reuse_owner_across_property_versions() { + let (_, tokens_withdrawn) = ingest_v1_events(&[ + token_withdraw_event(ALICE, "0"), + token_withdraw_event(BOB, "1"), + ]); + let empty_owners = AHashMap::new(); + + let alice_row = TokenOwnershipV2::get_v1_from_delete_table_item( + &delete_token_table_item("0xaaa", "0"), + 1, + 0, + ts(), + &empty_owners, + &tokens_withdrawn, + ) + .unwrap() + .expect("pv0 withdraw fallback") + .1 + .expect("current ownership"); + let bob_row = TokenOwnershipV2::get_v1_from_delete_table_item( + &delete_token_table_item("0xbbb", "1"), + 1, + 1, + ts(), + &empty_owners, + &tokens_withdrawn, + ) + .unwrap() + .expect("pv1 withdraw fallback") + .1 + .expect("current ownership"); + + assert_eq!( + alice_row.owner_address, + standardize_address(ALICE), + "pv0 must keep Alice, not the later Bob withdraw" + ); + assert_eq!(bob_row.owner_address, standardize_address(BOB)); + assert_eq!(alice_row.amount, BigDecimal::zero()); + assert_eq!(bob_row.amount, BigDecimal::zero()); + } +} From f474bf7e3bb95d499d179e5abb092ec1830339ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 17:59:11 +0000 Subject: [PATCH 2/3] Fix TokenStore PV tests for this SDK proto layout WriteTableItem/DeleteTableItem data is WriteTableData/DeleteTableData in aptos-protos 1.3.1, not nested write_table_item::Data modules. Co-authored-by: Young Yang Liauw --- .../token_v2/token_v2_models/v2_token_ownerships.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs b/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs index 7c0b3282..7f985422 100644 --- a/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs +++ b/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs @@ -850,7 +850,7 @@ mod tests { token_v2_models::v2_token_activities::TokenActivityV2, }; use aptos_indexer_processor_sdk::aptos_protos::transaction::v1::{ - delete_table_item, write_table_item, DeleteTableItem, Event, EventKey, WriteTableItem, + DeleteTableData, DeleteTableItem, Event, EventKey, WriteTableData, WriteTableItem, }; const CREATOR: &str = "0x1"; @@ -909,7 +909,7 @@ mod tests { WriteTableItem { handle: handle.to_string(), key: token_id_json(property_version), - data: Some(write_table_item::Data { + data: Some(WriteTableData { key: token_id_json(property_version), key_type: "0x3::token::TokenId".to_string(), value: token_json, @@ -923,11 +923,9 @@ mod tests { DeleteTableItem { handle: handle.to_string(), key: token_id_json(property_version), - data: Some(delete_table_item::Data { + data: Some(DeleteTableData { key: token_id_json(property_version), key_type: "0x3::token::TokenId".to_string(), - value: String::new(), - value_type: String::new(), }), ..Default::default() } From ff1ab0ceea968144b85eada6c987d8662a4e50be Mon Sep 17 00:00:00 2001 From: Young Yang Liauw <7528420+sausagee@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:34:50 -0700 Subject: [PATCH 3/3] ci: apply #70 unblocker files (stable xclippy + bookworm docker) Stack the CI unblocker files from PR #70 (stable cargo xclippy, bookworm Dockerfiles with make) onto this bugfix branch so CI can go green while #70 awaits review. Merge target remains main; this does not merge #70. --- Dockerfile | 22 +++++++++++++++++----- Dockerfile.address-reputation-api | 22 +++++++++++++++++----- scripts/rust_lint.sh | 5 ++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index a7e266e6..57df1080 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,25 @@ # Stage 1: Build the binary -FROM rust:slim-bullseye as builder +FROM rust:slim-bookworm AS builder WORKDIR /app COPY --link . /app -RUN for i in 1 2 3; do apt-get update && apt-get install --fix-missing -y cmake curl clang git pkg-config libssl-dev libdw-dev libpq-dev lld && break || sleep 10; done +RUN apt-get update \ + && apt-get install --no-install-recommends --fix-missing -y \ + cmake \ + curl \ + clang \ + make \ + git \ + pkg-config \ + libssl-dev \ + libdw-dev \ + libpq-dev \ + lld \ + && rm -rf /var/lib/apt/lists/* ENV CARGO_NET_GIT_FETCH_WITH_CLI true # TODO: Fix this with real processors. RUN cargo build --locked --release -p processor && ls -lah target/release/ @@ -24,19 +36,19 @@ ENV GIT_SHA ${GIT_SHA} # Stage 2: Create the final image -FROM debian:bullseye-slim +FROM debian:bookworm-slim COPY --from=builder /usr/local/bin/processor /usr/local/bin RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install --no-install-recommends --fix-missing -y \ - libssl1.1 \ + libssl3 \ ca-certificates \ net-tools \ tcpdump \ iproute2 \ - netcat \ + netcat-openbsd \ libdw-dev \ libpq-dev \ curl diff --git a/Dockerfile.address-reputation-api b/Dockerfile.address-reputation-api index 5be4c111..488d8ed5 100644 --- a/Dockerfile.address-reputation-api +++ b/Dockerfile.address-reputation-api @@ -2,13 +2,25 @@ # Stage 1: Build the binary -FROM rust:slim-bullseye as builder +FROM rust:slim-bookworm AS builder WORKDIR /app COPY --link . /app -RUN for i in 1 2 3; do apt-get update && apt-get install --fix-missing -y cmake curl clang git pkg-config libssl-dev libdw-dev libpq-dev lld && break || sleep 10; done +RUN apt-get update \ + && apt-get install --no-install-recommends --fix-missing -y \ + cmake \ + curl \ + clang \ + make \ + git \ + pkg-config \ + libssl-dev \ + libdw-dev \ + libpq-dev \ + lld \ + && rm -rf /var/lib/apt/lists/* ENV CARGO_NET_GIT_FETCH_WITH_CLI true RUN cargo build --locked --release -p address-reputation-api && ls -lah target/release/ RUN cp target/release/address-reputation-api /usr/local/bin @@ -23,19 +35,19 @@ ENV GIT_SHA ${GIT_SHA} # Stage 2: Create the final image -FROM debian:bullseye-slim +FROM debian:bookworm-slim COPY --from=builder /usr/local/bin/address-reputation-api /usr/local/bin RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install --no-install-recommends --fix-missing -y \ - libssl1.1 \ + libssl3 \ ca-certificates \ net-tools \ tcpdump \ iproute2 \ - netcat \ + netcat-openbsd \ libdw-dev \ libpq-dev \ curl diff --git a/scripts/rust_lint.sh b/scripts/rust_lint.sh index 261c1aa9..32a48f2b 100755 --- a/scripts/rust_lint.sh +++ b/scripts/rust_lint.sh @@ -25,7 +25,10 @@ fi set -e set -x -cargo +nightly xclippy +# Run clippy on the pinned STABLE toolchain (from rust-toolchain.toml), NOT +# nightly. Latest nightly makes Infallible an alias of !, which breaks +# allocative (impl Allocative for both). Stable clippy matches the build toolchain. +cargo xclippy # We require the nightly build of cargo fmt # to provide stricter rust formatting.