Skip to content

Commit a4cb5e3

Browse files
committed
add address repucation processor.
1 parent 4d69afb commit a4cb5e3

17 files changed

Lines changed: 1489 additions & 0 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Example AddressReputationProcessor config for Movement MAINNET.
2+
# Bridge addresses below are placeholders — fill in the real mainnet USDCx
3+
# deployment address and LayerZero OFT modules before running.
4+
5+
processor_mode:
6+
type: default
7+
initial_starting_version: 0
8+
9+
server_config:
10+
processor_config:
11+
type: address_reputation_processor
12+
channel_size: 10
13+
decay: 0.8
14+
bridges:
15+
# Circle USDCx bridge on Movement mainnet. Replace module_address with
16+
# the published `circle_remote::usdcx` address (which is also the USDCx
17+
# FA metadata address by Move design).
18+
- name: circle_usdcx
19+
module_address: "0x0000000000000000000000000000000000000000000000000000000000000000"
20+
event_type: "0x0000000000000000000000000000000000000000000000000000000000000000::usdcx::Mint"
21+
recipient_field_path: recipient
22+
amount_field_path: amount
23+
chain_id_field_path: remote_domain
24+
evm_source_field_path: null
25+
enabled: false # flip to true after addresses are filled in
26+
27+
transaction_stream_config:
28+
indexer_grpc_data_service_address: "https://grpc.mainnet.movementnetwork.xyz:443"
29+
auth_token: REPLACE_ME
30+
31+
db_config:
32+
type: postgres_config
33+
connection_string: postgresql://postgres:postgres@localhost:5432/address_reputation
34+
db_pool_size: 32
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# AddressReputationProcessor — Movement TESTNET deployment.
2+
#
3+
# Bridge addresses below were resolved against the live testnet indexer
4+
# (https://indexer.testnet.movementnetwork.xyz/v1/graphql) by inspecting real
5+
# captured events:
6+
# - Circle USDCx: event payload shape from txn 158025629
7+
# (../../usdc-bridge/default_config.yaml::usdcx_token_address).
8+
# - LayerZero OFT: event shape `<module>::oft_core::OftReceived` with fields
9+
# {guid, src_eid, to_address, amount_received_ld}, verified
10+
# on testnet txns 14291481, 14292998, 14293016, 126884140.
11+
#
12+
# `src_eid` values are LayerZero endpoint IDs (40102=BNB testnet, 40108=Avalanche
13+
# Fuji, etc — full list: docs.layerzero.network).
14+
15+
processor_mode:
16+
type: default
17+
initial_starting_version: 158000000
18+
19+
server_config:
20+
processor_config:
21+
type: address_reputation_processor
22+
channel_size: 10
23+
decay: 0.8
24+
bridges:
25+
# --- Circle USDCx ---------------------------------------------------
26+
- name: circle_usdcx
27+
module_address: "0x989577931ff5ec0575071a8bc9084c1c010981169e08cc29e1f82563ed03cafc"
28+
event_type: "0x989577931ff5ec0575071a8bc9084c1c010981169e08cc29e1f82563ed03cafc::usdcx::Mint"
29+
recipient_field_path: recipient
30+
amount_field_path: amount
31+
chain_id_field_path: remote_domain
32+
evm_source_field_path: null # carried in the entry function arg, not the event
33+
enabled: true
34+
35+
# --- LayerZero OFTs -------------------------------------------------
36+
# One row per OFT module. The module address is the FA creator (and
37+
# publisher of the `oft_core` Move module).
38+
39+
- name: layerzero_weth_e
40+
module_address: "0x2fa1f2914aa17d239410cb81ab46dd8fa9230272c58bc84e9e8b971eded79ca5"
41+
event_type: "0x2fa1f2914aa17d239410cb81ab46dd8fa9230272c58bc84e9e8b971eded79ca5::oft_core::OftReceived"
42+
recipient_field_path: to_address
43+
amount_field_path: amount_received_ld
44+
chain_id_field_path: src_eid
45+
evm_source_field_path: null # OftReceived only carries `guid`, not the EVM sender
46+
enabled: true
47+
48+
- name: layerzero_usdt_e
49+
module_address: "0x9cda672762a6f88e4b608428dd063e03aaf6712f0a427923dd0f1416afa1c075"
50+
event_type: "0x9cda672762a6f88e4b608428dd063e03aaf6712f0a427923dd0f1416afa1c075::oft_core::OftReceived"
51+
recipient_field_path: to_address
52+
amount_field_path: amount_received_ld
53+
chain_id_field_path: src_eid
54+
evm_source_field_path: null
55+
enabled: true
56+
57+
- name: layerzero_usdc_e
58+
module_address: "0x33987308d6698c3def1f155c8ea394360e9756b0a22e64fb20834327f04a1e42"
59+
event_type: "0x33987308d6698c3def1f155c8ea394360e9756b0a22e64fb20834327f04a1e42::oft_core::OftReceived"
60+
recipient_field_path: to_address
61+
amount_field_path: amount_received_ld
62+
chain_id_field_path: src_eid
63+
evm_source_field_path: null
64+
enabled: true
65+
66+
# Dormant USDC.e variant: module is published but no inbound OftReceived
67+
# events observed on testnet at the time of writing. Disabled by default
68+
# so it's silent until activity shows up.
69+
- name: layerzero_usdc_e_alt
70+
module_address: "0xdbfc7ba6398103cf01b65066402f7ec22fec40be31c5250d9928c6ff86a146e8"
71+
event_type: "0xdbfc7ba6398103cf01b65066402f7ec22fec40be31c5250d9928c6ff86a146e8::oft_core::OftReceived"
72+
recipient_field_path: to_address
73+
amount_field_path: amount_received_ld
74+
chain_id_field_path: src_eid
75+
evm_source_field_path: null
76+
enabled: false
77+
78+
# LayerZero SendOftDemo (SOD). Demo / test OFT, kept for completeness;
79+
# safe to leave on since it's a real OFT-style bridge.
80+
- name: layerzero_send_oft_demo
81+
module_address: "0xe024846967ef05baed04aa2396802b360dcd975fc704bfd261089cf5355669ad"
82+
event_type: "0xe024846967ef05baed04aa2396802b360dcd975fc704bfd261089cf5355669ad::oft_core::OftReceived"
83+
recipient_field_path: to_address
84+
amount_field_path: amount_received_ld
85+
chain_id_field_path: src_eid
86+
evm_source_field_path: null
87+
enabled: true
88+
89+
transaction_stream_config:
90+
indexer_grpc_data_service_address: "https://grpc.testnet.movementnetwork.xyz:443"
91+
auth_token: REPLACE_ME
92+
93+
db_config:
94+
type: postgres_config
95+
connection_string: postgresql://postgres:postgres@localhost:5432/address_reputation
96+
db_pool_size: 32

processor/src/config/indexer_processor_config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::{
2020
processors::{
2121
account_restoration::account_restoration_processor::AccountRestorationProcessor,
2222
account_transactions::account_transactions_processor::AccountTransactionsProcessor,
23+
address_reputation::address_reputation_processor::AddressReputationProcessor,
2324
ans::ans_processor::AnsProcessor, default::default_processor::DefaultProcessor,
2425
events::events_processor::EventsProcessor,
2526
fungible_asset::fungible_asset_processor::FungibleAssetProcessor,
@@ -57,6 +58,10 @@ impl RunnableConfig for IndexerProcessorConfig {
5758
let acc_txns_processor = AccountTransactionsProcessor::new(self.clone()).await?;
5859
acc_txns_processor.run_processor().await
5960
},
61+
ProcessorConfig::AddressReputationProcessor(_) => {
62+
let addr_rep_processor = AddressReputationProcessor::new(self.clone()).await?;
63+
addr_rep_processor.run_processor().await
64+
},
6065
ProcessorConfig::AnsProcessor(_) => {
6166
let ans_processor = AnsProcessor::new(self.clone()).await?;
6267
ans_processor.run_processor().await

processor/src/config/processor_config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
},
1111
processors::{
1212
account_transactions::account_transactions_model::ParquetAccountTransaction,
13+
address_reputation::address_reputation_config::AddressReputationConfig,
1314
ans::{
1415
ans_processor::AnsProcessorConfig,
1516
models::{
@@ -102,6 +103,7 @@ use std::collections::HashSet;
102103
pub enum ProcessorConfig {
103104
AccountRestorationProcessor(DefaultProcessorConfig),
104105
AccountTransactionsProcessor(DefaultProcessorConfig),
106+
AddressReputationProcessor(AddressReputationConfig),
105107
AnsProcessor(AnsProcessorConfig),
106108
DefaultProcessor(DefaultProcessorConfig),
107109
EventsProcessor(DefaultProcessorConfig),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP TABLE IF EXISTS address_reputation;
2+
DROP TABLE IF EXISTS evm_address_risk_scores;
3+
DROP TABLE IF EXISTS bridge_inflows;
4+
DROP TABLE IF EXISTS address_transfer_edges;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
-- Address reputation processor (P0): transfer graph + bridge inflows + per-address score.
2+
3+
-- One row per FA / coin transfer between two distinct Aptos addresses.
4+
CREATE TABLE IF NOT EXISTS address_transfer_edges (
5+
transaction_version BIGINT NOT NULL,
6+
event_index BIGINT NOT NULL,
7+
from_address VARCHAR(66) NOT NULL,
8+
to_address VARCHAR(66) NOT NULL,
9+
asset_type VARCHAR(1100),
10+
amount NUMERIC NOT NULL,
11+
is_bridge_inflow BOOLEAN NOT NULL DEFAULT FALSE,
12+
bridge_name VARCHAR(64),
13+
transaction_timestamp TIMESTAMP NOT NULL,
14+
inserted_at TIMESTAMP NOT NULL DEFAULT NOW(),
15+
PRIMARY KEY (transaction_version, event_index)
16+
);
17+
CREATE INDEX IF NOT EXISTS idx_ate_to_ts ON address_transfer_edges (to_address, transaction_timestamp);
18+
CREATE INDEX IF NOT EXISTS idx_ate_from_ts ON address_transfer_edges (from_address, transaction_timestamp);
19+
CREATE INDEX IF NOT EXISTS idx_ate_bridge ON address_transfer_edges (bridge_name) WHERE is_bridge_inflow;
20+
21+
-- (Bridge configuration lives in the processor's YAML config, not in the database.
22+
-- This keeps mainnet/testnet/devnet deploys swappable without a SQL migration. See
23+
-- AddressReputationConfig.bridges in processor/src/processors/address_reputation/address_reputation_config.rs.)
24+
25+
-- Bridge deposit events on Aptos. The cross-chain EVM source is the "head" of any trace.
26+
CREATE TABLE IF NOT EXISTS bridge_inflows (
27+
transaction_version BIGINT NOT NULL,
28+
event_index BIGINT NOT NULL,
29+
bridge_name VARCHAR(64) NOT NULL,
30+
aptos_recipient VARCHAR(66) NOT NULL,
31+
evm_source VARCHAR(66),
32+
src_chain_id INTEGER,
33+
asset_type VARCHAR(1100),
34+
amount NUMERIC NOT NULL,
35+
transaction_timestamp TIMESTAMP NOT NULL,
36+
inserted_at TIMESTAMP NOT NULL DEFAULT NOW(),
37+
PRIMARY KEY (transaction_version, event_index)
38+
);
39+
CREATE INDEX IF NOT EXISTS idx_bi_recipient ON bridge_inflows (aptos_recipient);
40+
CREATE INDEX IF NOT EXISTS idx_bi_evm_source ON bridge_inflows (evm_source);
41+
42+
-- Risk scores for EVM addresses, populated by an external screening pipeline.
43+
CREATE TABLE IF NOT EXISTS evm_address_risk_scores (
44+
evm_address VARCHAR(66) NOT NULL PRIMARY KEY,
45+
risk_score NUMERIC(5,4) NOT NULL,
46+
risk_label VARCHAR(32),
47+
source VARCHAR(64),
48+
fetched_at TIMESTAMP NOT NULL DEFAULT NOW(),
49+
inserted_at TIMESTAMP NOT NULL DEFAULT NOW()
50+
);
51+
52+
-- Materialized per-address reputation. Updated on every new inbound edge.
53+
CREATE TABLE IF NOT EXISTS address_reputation (
54+
address VARCHAR(66) NOT NULL PRIMARY KEY,
55+
score NUMERIC(5,4) NOT NULL DEFAULT 0,
56+
highest_seed NUMERIC(5,4) NOT NULL DEFAULT 0,
57+
nearest_seed_hop INTEGER,
58+
last_updated_version BIGINT NOT NULL,
59+
last_updated_timestamp TIMESTAMP NOT NULL,
60+
inserted_at TIMESTAMP NOT NULL DEFAULT NOW()
61+
);
62+
CREATE INDEX IF NOT EXISTS idx_ar_score ON address_reputation (score DESC);
63+
64+
-- Seed: Circle USDCx bridge (Movement's bridged USDC, ../usdc-bridge/smart_contracts/sources/usdcx.move).
65+
-- The Mint event is emitted when funds arrive from a remote chain (USDC -> USDCx mint).
66+
-- Fields available in the event:
67+
-- recipient (address, Movement-side owner), amount (u64), fee_amount, relayer,
68+
-- remote_domain (u32, source chain id), remote_token (address, source-chain USDC), nonce.
69+
-- The EVM depositor address is NOT in the event payload -- it is carried inside the
70+
-- `intent_payload` entry function argument and would need BCS-decoding to recover.
71+
-- For P0 we record the inflow with NULL evm_source; downstream can backfill it.
72+
--

processor/src/db/queries/trace.sql

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- Backward trace from an Aptos address through transfer edges.
2+
-- Stops when an edge marked is_bridge_inflow is reached, or when max depth is exceeded.
3+
-- Params: $1 = target address, $2 = max depth (use a large int for "full trace").
4+
5+
WITH RECURSIVE upstream AS (
6+
SELECT
7+
e.to_address AS sink,
8+
e.from_address AS source,
9+
e.transaction_version,
10+
e.event_index,
11+
e.amount,
12+
e.is_bridge_inflow,
13+
e.bridge_name,
14+
0 AS depth
15+
FROM address_transfer_edges e
16+
WHERE e.to_address = $1
17+
UNION ALL
18+
SELECT
19+
e.to_address,
20+
e.from_address,
21+
e.transaction_version,
22+
e.event_index,
23+
e.amount,
24+
e.is_bridge_inflow,
25+
e.bridge_name,
26+
u.depth + 1
27+
FROM address_transfer_edges e
28+
JOIN upstream u ON e.to_address = u.source
29+
WHERE u.depth < $2 AND NOT u.is_bridge_inflow
30+
)
31+
SELECT
32+
sink,
33+
source,
34+
transaction_version,
35+
event_index,
36+
amount,
37+
is_bridge_inflow,
38+
bridge_name,
39+
depth
40+
FROM upstream
41+
ORDER BY depth, transaction_version, event_index;

processor/src/db/schema.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,8 +1357,77 @@ diesel::table! {
13571357
}
13581358
}
13591359

1360+
diesel::table! {
1361+
address_reputation (address) {
1362+
#[max_length = 66]
1363+
address -> Varchar,
1364+
score -> Numeric,
1365+
highest_seed -> Numeric,
1366+
nearest_seed_hop -> Nullable<Int4>,
1367+
last_updated_version -> Int8,
1368+
last_updated_timestamp -> Timestamp,
1369+
inserted_at -> Timestamp,
1370+
}
1371+
}
1372+
1373+
diesel::table! {
1374+
address_transfer_edges (transaction_version, event_index) {
1375+
transaction_version -> Int8,
1376+
event_index -> Int8,
1377+
#[max_length = 66]
1378+
from_address -> Varchar,
1379+
#[max_length = 66]
1380+
to_address -> Varchar,
1381+
#[max_length = 1100]
1382+
asset_type -> Nullable<Varchar>,
1383+
amount -> Numeric,
1384+
is_bridge_inflow -> Bool,
1385+
#[max_length = 64]
1386+
bridge_name -> Nullable<Varchar>,
1387+
transaction_timestamp -> Timestamp,
1388+
inserted_at -> Timestamp,
1389+
}
1390+
}
1391+
1392+
diesel::table! {
1393+
bridge_inflows (transaction_version, event_index) {
1394+
transaction_version -> Int8,
1395+
event_index -> Int8,
1396+
#[max_length = 64]
1397+
bridge_name -> Varchar,
1398+
#[max_length = 66]
1399+
aptos_recipient -> Varchar,
1400+
#[max_length = 66]
1401+
evm_source -> Nullable<Varchar>,
1402+
src_chain_id -> Nullable<Int4>,
1403+
#[max_length = 1100]
1404+
asset_type -> Nullable<Varchar>,
1405+
amount -> Numeric,
1406+
transaction_timestamp -> Timestamp,
1407+
inserted_at -> Timestamp,
1408+
}
1409+
}
1410+
1411+
diesel::table! {
1412+
evm_address_risk_scores (evm_address) {
1413+
#[max_length = 66]
1414+
evm_address -> Varchar,
1415+
risk_score -> Numeric,
1416+
#[max_length = 32]
1417+
risk_label -> Nullable<Varchar>,
1418+
#[max_length = 64]
1419+
source -> Nullable<Varchar>,
1420+
fetched_at -> Timestamp,
1421+
inserted_at -> Timestamp,
1422+
}
1423+
}
1424+
13601425
diesel::allow_tables_to_appear_in_same_query!(
13611426
account_transactions,
1427+
address_reputation,
1428+
address_transfer_edges,
1429+
bridge_inflows,
1430+
evm_address_risk_scores,
13621431
ans_lookup,
13631432
ans_lookup_v2,
13641433
ans_primary_name,

0 commit comments

Comments
 (0)