@guaritos/tracer-engine
Version:
A highly performant and scalable multi-hop, time-aware tracer for account-based blockchain transactions, designed for off-chain risk assessment and flow analysis.
492 lines • 19.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TTRRedirect = exports.TTR = void 0;
const ttr_defs_1 = require("../items/ttr_defs");
const push_pop_1 = require("./push_pop");
class TTR extends push_pop_1.PushPopModel {
constructor(source, alpha = 0.15, beta = 0.7, epsilon = 1e-3) {
super(source);
this.alpha = alpha;
this.beta = beta;
this.epsilon = epsilon;
this.p = {};
this.r = new Map();
this.weighted_edges = [];
}
push(node, edges, ...kwargs) {
throw new Error("Method not implemented.");
}
pop() {
throw new Error("Method not implemented.");
}
get_context_snapshot() {
return {
source: this.source,
alpha: this.alpha,
beta: this.beta,
epsilon: this.epsilon,
r: this.r,
p: this.p,
weighted_edges: this.weighted_edges,
};
}
get_node_rank() {
return this.p;
}
}
exports.TTR = TTR;
class TTRRedirect extends TTR {
constructor(source, alpha = 0.15, beta = 0.7, epsilon = 1e-3) {
super(source, alpha, beta, epsilon);
this._vis = new Set();
this._added_edges_hash = new Set();
}
push(node, edges) {
// if residual vector is none, add empty list
if (!(this.r.get(node))) {
this.r.set(node, []);
}
// push on first time
if (node === this.source && !this._vis.has(this.source)) {
this._vis.add(this.source);
// calc value of each symbol
const in_sum = new Map();
const out_sum = new Map();
const symbols = new Set();
for (const e of edges) {
symbols.add(e.symbol);
if (e.to === this.source) {
in_sum.set(e.symbol, (in_sum.get(e.symbol) || 0) + e.value);
}
else if (e.from === this.source) {
out_sum.set(e.symbol, (out_sum.get(e.symbol) || 0) + e.value);
}
}
// first self push
this.p[this.source] = this.alpha * symbols.size;
// first forward and backward push
for (const e of edges) {
if (e.from === this.source && (out_sum.get(e.symbol) || 0) !== 0) {
if (!this.r.has(e.to)) {
this.r.set(e.to, []);
}
const value = (1 - this.alpha) * this.beta * e.value / out_sum.get(e.symbol);
if (value > 0) {
this.r.get(e.to)?.push({
value: value,
symbol: e.symbol,
timestamp: e.timestamp,
});
this.weighted_edges.push({
from: node,
to: e.to,
weight: value,
symbol: e.symbol,
hash: e.hash,
timestamp: e.timestamp,
metadata: e.metadata,
});
}
}
else if (e.to === this.source && (in_sum.get(e.symbol) || 0) !== 0) {
if (!this.r.has(e.from)) {
this.r.set(e.from, []);
}
const value = (1 - this.alpha) * (1 - this.beta) * e.value / in_sum.get(e.symbol);
if (value > 0) {
this.r.get(e.from)?.push({
value: value,
symbol: e.symbol,
timestamp: e.timestamp,
});
this.weighted_edges.push({
from: e.from,
to: node,
weight: value,
symbol: e.symbol,
hash: e.hash,
timestamp: e.timestamp,
metadata: e.metadata,
});
}
}
}
for (const symbol of symbols) {
if ((out_sum.get(symbol) || 0) === 0) {
this.r.get(this.source).push({
symbol: symbol,
value: (1 - this.alpha) * this.beta,
timestamp: 0,
});
}
else if ((in_sum.get(symbol) || 0) === 0) {
this.r.get(this.source).push({
symbol: symbol,
value: (1 - this.alpha) * (1 - this.beta),
timestamp: Number.MAX_SAFE_INTEGER,
});
}
}
for (const e of edges) {
this._added_edges_hash.add(e.hash);
}
return;
}
// copy residual vector with sort and clear
let r = this.r.get(node) || [];
r.sort((a, b) => b.timestamp - a.timestamp);
this.r.set(node, []);
// filter added edges
edges = edges.filter(e => !this._added_edges_hash.has(e.hash));
// aggregate edges
let agg_es = this._get_aggregated_edges(node, edges);
agg_es.sort((a, b) => b.get_timestamp() - a.get_timestamp());
// mark edges as added after having been aggregated
for (const e of edges) {
this._added_edges_hash.add(e.hash);
}
// push
this._self_push(node, r);
this._forward_push(node, agg_es, r);
this._backward_push(node, agg_es, r);
// merge chips
for (const [node, chips] of this.r.entries()) {
const _chips = new Map();
for (const chip of chips) {
const key = `${chip.symbol},${chip.timestamp}`;
if (!_chips.has(key)) {
_chips.set(key, chip);
continue;
}
_chips.get(key).value += chip.value;
}
this.r.set(node, Array.from(_chips.values()));
}
}
_self_push(node, r) {
let sum_r = 0;
for (const chip of r) {
sum_r += chip.value;
}
this.p[node] = (this.p[node] || 0) + this.alpha * sum_r;
}
_forward_push(node, aggregated_edges, r) {
if (r.length === 0) {
return;
}
// calc the weight sum after each chip
let j = aggregated_edges.length - 1;
let sum_w = new Map();
let W = new Map();
for (let i = r.length - 1; i >= 0; i--) {
const c = r[i];
while (j >= 0 && aggregated_edges[j].get_timestamp() > c.timestamp) {
const e = aggregated_edges[j];
const profits = e.get_output_profits();
for (const profit of profits) {
sum_w.set(profit.symbol, (sum_w.get(profit.symbol) || 0) + profit.value);
}
j -= 1;
}
W.set(String(c), sum_w.get(c.symbol) || 0);
}
// construct index for distributing profit
const symbol_agg_es = new Map();
const symbol_agg_es_idx = new Map();
for (let i = 0; i < aggregated_edges.length; i++) {
const e = aggregated_edges[i];
for (const profit of e.get_output_profits()) {
if (!symbol_agg_es.has(profit.symbol)) {
symbol_agg_es.set(profit.symbol, []);
symbol_agg_es_idx.set(profit.symbol, []);
}
symbol_agg_es.get(profit.symbol).push(e);
symbol_agg_es_idx.get(profit.symbol).push(i);
}
}
const distributing_index = new Map();
for (const symbol of symbol_agg_es.keys()) {
const es_idx = symbol_agg_es_idx.get(symbol);
const index = Array.from({ length: aggregated_edges.length }, () => 0);
let j = 0;
for (let i = 0; i < index.length; i++) {
if (j < es_idx.length && es_idx[j] <= i) {
j += 1;
}
index[i] = j;
}
distributing_index.set(symbol, index);
}
// push residual to neighbors
j = 0;
const d = new Map();
for (let i = 0; i < aggregated_edges.length; i++) {
const e = aggregated_edges[i];
const output_profits = e.get_output_profits();
if (output_profits.length === 0) {
continue;
}
while (j < r.length && e.get_timestamp() > r[j].timestamp) {
const c = r[j];
const symbol = c.symbol;
const inc_d = W.get(String(c)) !== 0 ? (c.value / W.get(String(c))) : 0;
d.set(symbol, (d.get(symbol) || 0) + inc_d);
j += 1;
}
for (const profit of output_profits) {
const inc = (1 - this.alpha) * this.beta * profit.value * (d.get(profit.symbol) || 0);
if (inc === 0) {
continue;
}
const distributing_profits = this._get_distributing_profit(-1, profit.symbol, i, aggregated_edges, distributing_index, symbol_agg_es_idx, inc);
for (const dp of distributing_profits) {
if (this.r.get(dp.address) === undefined) {
this.r.set(dp.address, []);
}
this.r.get(dp.address).push({
value: inc / distributing_profits.length,
symbol: dp.symbol,
timestamp: dp.timestamp,
});
this.weighted_edges.push(new ttr_defs_1.WeightedEdge({
from: node,
to: dp.address,
weight: inc / distributing_profits.length,
symbol: dp.symbol,
hash: e.hash,
timestamp: e.get_timestamp(),
metadata: e.metadata,
}));
}
}
}
// recycle the residual without push
const cs = new Map();
while (j < r.length) {
const c = r[j];
const key = `${c.symbol},${c.timestamp}`;
cs.set(key, {
value: (cs.get(key)?.value || 0) + (1 - this.alpha) * this.beta * (c.value || 0),
symbol: c.symbol,
timestamp: c.timestamp,
});
j += 1;
}
for (const [key, value] of cs.entries()) {
this.r.get(node).push({
value: value.value,
symbol: value.symbol,
timestamp: value.timestamp,
});
}
}
_backward_push(node, aggregated_edges, r) {
if (r.length === 0) {
return;
}
// calc the weight sum before each chip
let j = 0;
const sum_w = new Map();
const W = new Map();
for (let i = 0; i < r.length; i++) {
const c = r[i];
while (j < aggregated_edges.length && aggregated_edges[j].get_timestamp() < c.timestamp) {
const e = aggregated_edges[j];
const profits = e.get_input_profits();
for (const profit of profits) {
sum_w.set(profit.symbol, (sum_w.get(profit.symbol) || 0) + profit.value);
}
j += 1;
}
W.set(i, sum_w.get(c.symbol) || 0);
}
// construct index for distributing profit
const symbol_agg_es = new Map();
const symbol_agg_es_idx = new Map();
for (let i = 0; i < aggregated_edges.length; i++) {
const e = aggregated_edges[i];
for (const profit of e.get_output_profits()) {
if (!symbol_agg_es.has(profit.symbol)) {
symbol_agg_es.set(profit.symbol, []);
symbol_agg_es_idx.set(profit.symbol, []);
}
symbol_agg_es.get(profit.symbol).push(e);
symbol_agg_es_idx.get(profit.symbol).push(i);
}
}
const distributing_index = new Map();
for (const symbol of symbol_agg_es.keys()) {
const es_idx = symbol_agg_es_idx.get(symbol);
const index = Array.from({ length: aggregated_edges.length }, () => 0);
let j = es_idx.length - 1;
for (let i = index.length - 1; i >= 0; i--) {
if (j > 0 && es_idx[j] >= i) {
j -= 1;
}
index[i] = j;
}
distributing_index.set(symbol, index);
}
// push residual to neighbors
j = r.length - 1;
const d = new Map();
for (let i = aggregated_edges.length - 1; i >= 0; i--) {
const e = aggregated_edges[i];
const input_profits = e.get_input_profits();
if (input_profits.length === 0) {
continue;
}
while (j >= 0 && e.get_timestamp() < r[j].timestamp) {
const c = r[j];
const symbol = c.symbol;
const inc_d = W.get(j) !== 0 ? (c.value / W.get(j)) : 0;
d.set(symbol, (d.get(symbol) || 0) + inc_d);
j -= 1;
for (const profit of input_profits) {
const inc = (1 - this.alpha) * (1 - this.beta) * profit.value * (d.get(profit.symbol) || 0);
if (inc === 0) {
continue;
}
const distributing_profits = this._get_distributing_profit(1, profit.symbol, i, aggregated_edges, distributing_index, symbol_agg_es_idx, inc);
for (const dp of distributing_profits) {
if (!this.r.get(dp.address)) {
this.r.set(dp.address, []);
}
this.r.get(dp.address).push({
value: inc / distributing_profits.length,
symbol: dp.symbol,
timestamp: dp.timestamp,
});
this.weighted_edges.push(new ttr_defs_1.WeightedEdge({
from: dp.address,
to: node,
weight: inc / distributing_profits.length,
symbol: dp.symbol,
hash: e.hash,
timestamp: e.get_timestamp(),
metadata: e.metadata,
}));
}
}
}
}
// recycle the residual without push
const cs = new Map();
while (j >= 0) {
const c = r[j];
const key = { symbol: c.symbol, timestamp: c.timestamp };
cs.set(key, (cs.get(key) || 0) + (1 - this.alpha) * (1 - this.beta) * (c.value || 0));
j -= 1;
}
for (const [key, value] of cs.entries()) {
if (!this.r.get(node)) {
this.r.set(node, []);
}
this.r.get(node).push(new ttr_defs_1.Profit(key.symbol, value, key.timestamp));
}
}
pop() {
let node = null;
let r = this.epsilon;
for (const [_node, chips] of this.r.entries()) {
let sum_r = 0;
for (const chip of chips) {
sum_r += chip.value;
}
if (sum_r > r) {
r = sum_r;
node = _node;
}
}
if (node === null) {
return [null, {}];
}
return [node, {
residual: r,
allow_all_tokens: true,
}];
}
get_context_snapshot() {
let data = TTR.prototype.get_context_snapshot.call(this);
const acc_r = {};
for (const [_node, chips] of this.r.entries()) {
acc_r[_node] = chips.reduce((sum, chip) => sum + chip.value, 0);
}
data.r = acc_r;
return data;
}
_get_distributing_profit(direction, symbol, index, aggregated_edges, distributing_index, symbol_agg_es_idx, chip_value) {
/**
* Get distributing profit for a specific direction and symbol.
* @param direction: 1 means input and -1 means output
* @param symbol: the symbol of the chip
* @param index: current aggregated edge index
* @param aggregated_edges: aggregated edges
* @param distributing_index: a dict to store distributing index
* @param symbol_agg_es_idx: a dict to store symbol aggregated edges index
* @param chip_value: the value of the chip
* @return: a list of profit
*/
let rlt = [];
let stack = [];
stack.push({ direction, symbol, index });
let vis = new Set();
while (stack.length > 0) {
const args = stack.pop();
if (vis.has(args)) {
continue;
}
vis.add(args);
const { direction, symbol, index } = args;
const cur_e = aggregated_edges[index];
const no_reverse_profits = cur_e.profits.filter(profit => profit.value * direction > 0);
const reverse_profits = cur_e.profits.filter(profit => profit.value * direction < 0);
if (stack.length > 0 && chip_value / stack.length < this.epsilon) {
return no_reverse_profits.filter(profit => profit.symbol === symbol);
}
if (reverse_profits.length === 1) {
const profit = reverse_profits[0];
const _symbol_agg_es_idx = symbol_agg_es_idx.get(profit.symbol);
const _distributing_index = distributing_index.get(profit.symbol);
if (_symbol_agg_es_idx === undefined || _distributing_index === undefined) {
continue;
}
let indices;
if (direction < 0) {
indices = _symbol_agg_es_idx.slice(_distributing_index[index]);
}
else {
indices = _symbol_agg_es_idx.slice(0, _distributing_index[index]);
}
for (const _index of indices) {
stack.push({ direction, symbol: profit.symbol, _index });
}
}
else {
rlt.push(...no_reverse_profits.filter(profit => profit.symbol === symbol));
}
}
return rlt;
}
_get_aggregated_edges(node, edges) {
/**
* Get aggregated edges for a specific node.
* @param node:
* @param edges: hash, from, to, value, timeStamp, symbol
* @return:
*/
let aggregated_edges = new Map();
for (const edge of edges) {
const hash = edge.hash;
let aggregated_edge = new ttr_defs_1.AggregatedEdge(hash, [new ttr_defs_1.AggregatedEdgeProfit(edge.from == node ? edge.to : edge.from, edge.from == node ? -edge.value : edge.value, edge.timestamp, edge.symbol)], [edge], edge.metadata);
aggregated_edge = aggregated_edge.aggregate(aggregated_edges.get(hash));
aggregated_edges.set(hash, aggregated_edge);
if (aggregated_edge.profits.length === 0) {
aggregated_edges.delete(hash);
}
}
return Array.from(aggregated_edges.values());
}
}
exports.TTRRedirect = TTRRedirect;
//# sourceMappingURL=ttr.js.map