链上取证与资金流追踪

从归档全节点、索引服务与原始 trace 三类数据源出发,系统讲解链上取证与资金流追踪方法:地址聚类的共同输入、找零识别、存款归集等启发式规则及其失效点,标签体系与实体识别,混币器与跨链桥造成的资金流断裂,资金流图谱的构图、剪枝与金额比例分配算法,行为指纹与时间序列分析,以及合规取证的证据链固化与报告产出流程。

区块链的透明性是双刃剑:每一笔转账都永久可查,但地址本身是假名的。链上取证要解决的核心问题就是把假名地址还原成现实实体,并把资金流动的路径还原成可解释、可举证的链条。

这不是纯技术问题。取证结果最终要用于司法或合规场景,因此「算法给出的结论」必须能被独立复现、能追溯到具体区块与交易哈希。本文从数据源讲起,逐层拆到聚类、图谱与报告产出。

取证的数据基础:三类数据源

取证工作对数据的要求比普通 DApp 高得多,因为它需要完整且可复现的历史。三类数据源各有边界:

数据源内容适用场景局限
归档全节点全量区块、交易、收据、状态精确重放与状态查询存储 TB 级,查询慢
trace / debug API内部调用、CREATE、SELFDESTRUCT、gas追踪合约内部的资金转移节点需开启 debug 且性能差
索引服务已解析的转账、事件、标签快速构图与批量分析解析口径可能与应用不一致

第一原则:以全节点为准。索引服务(无论自建还是第三方)的解析逻辑都可能漏掉边缘情况,例如:

  • ERC-20 转账事件里 value 为 0 但实际有状态变更。
  • 内部转账(合约调用合约再转账)在普通 eth_getBlockByNumber 里看不到,必须用 debug_traceTransaction 或 trace_block。
  • 代理合约(Proxy)的调用在 trace 里表现为 DELEGATECALL 链,直接看顶层调用会误判调用方。
# 拉取一笔交易的完整调用树
cast run 0xabc... --rpc-url $ARCHIVE_RPC --trace

# 用 erigon 的 trace API 拿内部转账
curl -s -X POST $ARCHIVE_RPC -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"trace_transaction","params":["0xabc..."]}' \
  | jq '.[] | {type: .traceAddress, from: .action.from, to: .action.to, value: .action.value}'

自建索引的取舍与实现细节可参考 链上数据索引与解析 ,取证场景下要特别关注重组(reorg)处理:一个在 12 个确认后才被回滚的交易,如果索引器没做深度确认,证据链会直接失效。

地址聚类:启发式规则与失效点

地址聚类是取证的地基。目标是把「同一个实体控制的一批地址」归为一类。主流启发式规则有四类:

共同输入所有权(Common Input Ownership)

比特币 UTXO 模型下,一笔交易的多个输入地址几乎必然由同一钱包控制(因为要同时签名)。这是最可靠的启发式,但在账户模型下完全不适用:以太坊一笔交易只有一个 from。

对 UTXO 链的实现:

# 伪代码:共同输入聚类
def cluster_utxo(txs):
    parent = {}
    def find(x):
        while parent.get(x, x) != x:
            parent[x] = parent.get(parent[x], parent[x])
            x = parent[x]
        return x
    def union(a, b):
        ra, rb = find(a), find(b)
        if ra != rb:
            parent[rb] = ra

    for tx in txs:
        inputs = [i.address for i in tx.inputs if not i.is_coinbase]
        for addr in inputs[1:]:
            union(inputs[0], addr)
    return parent

找零地址识别(Change Address Heuristics)

UTXO 交易通常有一个输出是找零。识别规则包括:地址首次出现、输出金额小于输入、地址类型与输入一致。一旦识别出找零,就能把找零地址归入同一实体。

失效场景:钱包软件更新导致地址类型变化(P2PKH 转 P2SH-P2WPKH),找零启发式会误判,把找零地址当成收款方,从而错误合并两个实体。

存款地址聚类(Deposit Address Clustering)

交易所为每个用户生成独立的充值地址,但这些地址最终都归集到同一批热钱包。识别路径是:多个地址向同一个地址转账且从无反向流动,则该地址可能是交易所归集地址。这条规则准确率很高,是识别交易所用户的常用手段。

Gas 与手续费出资(Gas Funding)

以太坊上,一个全新地址的第一笔 gas 往往来自某个「金主」。金主地址常被用来串联一批新地址。但这条规则的假阳性极高:混币器、空投分发、交易所提现都会造成大规模出资关系。实务中它只能作为辅助证据,不能单独作为聚类依据。

关键认知:所有启发式都是概率性的。取证结论必须标注置信度,并且允许被反证推翻。把启发式结果当成事实去指控,是取证中最严重的错误。

标签体系与实体识别

聚类把地址分组,标签则回答「这组地址是谁」。标签来源分三层:

  1. 公开来源:区块浏览器(Etherscan、Blockchair)的公开标签、官方公告的合约地址、ENS 域名反向解析。
  2. 商业标签库:Chainalysis、TRM、Elliptic 等厂商的实体库,覆盖交易所、混币器、暗网市场、勒索软件地址。
  3. 自建推断:基于行为特征的分类器,例如「高频小额进、大额出且时间规律」→ 可能是交易所充值地址。

自建分类器的一个可解释实现:

def classify_address(features):
    """基于规则的可解释分类,便于在报告中说明依据"""
    score = {}
    # 交易所归集特征
    if features.in_degree > 1000 and features.out_degree < 20:
        score["exchange_collector"] = 0.8
    # 混币器特征:大额进、等额拆分成固定面额出
    if features.equal_denomination_ratio > 0.7 and features.fan_out > 50:
        score["mixer"] = 0.9
    # 桥合约:与多个链的桥地址有对称流动
    if features.bridge_pair_count > 2:
        score["bridge"] = 0.85
    return score

标签体系最大的问题是时效性。交易所会轮换热钱包,混币服务会关闭或升级,一年前的标签可能已经指向完全不同的实体。工程上要给标签打时间戳,取证时只使用「与案发时间窗口一致」的标签。

混币、跨链桥与资金流断裂

现代洗钱链路的标准形态是「分层(layering)」,目标是打断地址间的可追踪关联:

受害地址 → 交易所充值 → 提现到新地址 → 混币器 → 跨链桥 → 另一条链 → DEX 换币 → 法币出金

混币器的追踪思路

以 Tornado Cash 为例,它用零知识证明切断了存取款地址的链上关联。但金额与时间的统计关联仍然存在:

  • 存款和取款金额在固定面额(0.1 / 1 / 10 / 100 ETH)下匹配,池子小时可通过时间窗口缩小候选集。
  • 若一个地址在案发后短时间内存入了精确匹配的金额,且在同池的取款记录中能找到时间接近的候选,则相关性显著。
  • 合规侧的解法更彻底:Tornado 被制裁后,主流做法是拒绝任何与受制裁池有直接交互的地址,把问题转化为合规名单问题而非追踪问题。

跨链桥的追踪

跨链桥是资金流断裂的主要发生地,因为桥的两端在不同的链上,且很多桥用中继者垫资(liquidity pool 模式),两端没有直接的交易对应关系。

追踪要点:

  • 锁定-铸造型桥:源链 lock 事件与目标链 mint 事件在金额上对应,可通过金额 + 时间窗口匹配。这是最容易追的。
  • 流动性池型桥:源链存入池子,目标链从另一个池子取出,两端由做市商垫付。此时桥的内部账本才是关键,链上只能看到池子的净流入流出。
  • 跨链消息型桥(如 IBC、LayerZero):两端靠消息证明关联,追踪需要解析桥自身的消息日志。

不同桥的信任模型与追踪难度差异,可对照 跨链互操作全景 中的分类。

# 跨链桥配对:按金额与时间窗口匹配两端事件
def match_bridge_events(src_events, dst_events, window=600):
    matches = []
    for s in src_events:
        for d in dst_events:
            if abs(s.amount - d.amount) <= s.amount * 0.001 \
               and 0 < d.timestamp - s.timestamp <= window:
                matches.append((s.tx_hash, d.tx_hash, d.timestamp - s.timestamp))
    return matches

局限:如果资金在桥内被拆分成多笔、或经过做市商的中转地址,简单的金额匹配就会失效,必须结合桥的内部账本或申请链下数据。

行为指纹与时间序列分析

资金流图告诉你「钱去了哪里」,行为指纹告诉你「这是谁」。当聚类与标签都失效时,行为特征往往是最后的抓手。

可提取的特征维度:

维度特征取证含义
时间节律交易间隔的分布、活跃时段与某地区作息或自动化脚本对应
金额分布金额尾数、是否取整、是否固定面额固定面额强烈暗示混币器
Gas 行为gas price 取值习惯、是否用默认值同一钱包软件的指纹
Nonce 模式nonce 是否连续、有无跳号判断是否同一账户被接管
交互对象常调用的合约集合行为画像的核心

时间序列上最有效的分析是案发时间窗对齐。假设案件在 T 时刻发生,把所有相关地址在 [T, T+Δ] 内的行为按分钟聚合,画成时间线,往往能看到明显的同步动作:

import pandas as pd

def build_timeline(transfers, t0, window_minutes=120):
    df = pd.DataFrame(transfers)
    df["ts"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)
    df = df[(df["ts"] >= t0) & (df["ts"] <= t0 + pd.Timedelta(minutes=window_minutes))]
    # 按分钟分桶,统计转账笔数与金额
    buckets = df.set_index("ts").resample("1min").agg(
        tx_count=("tx_hash", "count"),
        value_sum=("value", "sum"),
    )
    return buckets

一个真实模式:攻击者在同一分钟内启动数十个新地址,每个地址从同一个 gas 出资方拿到启动资金,然后各自发起一笔金额相同的转账。这种同步爆发在时间序列上极其显眼,比图分析更快定位到攻击者控制的地址簇。

行为指纹的局限在于可伪造。熟练的攻击者会刻意打散时间间隔、随机化金额、用不同的 gas 策略。因此行为特征更适合作为「缩小候选集」的手段,而非最终定案依据。真正定案仍然要回到资金流的逐笔举证。

资金流图谱分析

把地址和转账抽象成有向带权图后,取证问题就变成了图算法问题。

构图要素:

  • 节点:地址或聚类后的实体。
  • 边:转账,权重为金额,方向为流向。
  • 时间维度:每条边带时间戳,分析时通常限定在案发时间窗口内。

核心算法:

  1. 资金溯源(tracing):从受害地址出发,沿出边 BFS,遇到交易所或混币器停止。实务中要做金额比例分配——若一笔资金被拆成 3 份流向不同地址,每条下游路径只继承相应比例,而不是全额标记。
  2. 环路检测:自转账环路(A→B→A)常被用来伪造交易量或混淆来源,用 Tarjan 或 DFS 检测强连通分量。
  3. 社区发现:用 Louvain 等算法在图上做社区划分,与已知标签交叉验证,能发现聚类规则漏掉的关系。
  4. 剪枝:真实数据里交易所归集地址会有百万级出边,必须先按金额阈值与「是否与案件地址有关联路径」剪枝,否则图会爆炸。
import networkx as nx

def trace_with_pruning(G, source, sinks, max_depth=6, min_value=0.01):
    """带金额衰减与剪枝的资金溯源"""
    flows = {}   # 记录每条路径继承的比例
    stack = [(source, 1.0, [source])]
    while stack:
        node, ratio, path = stack.pop()
        if node in sinks:
            flows[tuple(path)] = ratio
            continue
        if len(path) > max_depth:
            continue
        out_edges = list(G.out_edges(node, data=True))
        total = sum(d["value"] for _, _, d in out_edges) or 1
        for _, nxt, d in out_edges:
            share = ratio * (d["value"] / total)
            if share >= min_value and nxt not in path:   # 防环
                stack.append((nxt, share, path + [nxt]))
    return flows

一个易被忽略的细节:图的规模控制。一次大型交易所被盗案件涉及几十万地址、上百万条边,全量载入内存会 OOM。工程上要做分层:先用聚合视图定位关键路径,再对关键路径做逐笔展开。这与 MEV 与区块构建 里分析搜索者行为时面临的图规模问题同源。

合规取证流程与报告产出

取证的技术结果要转化为可采信的证据,流程必须固化:

  1. 数据固化:在案发时间点对相关区块高度做快照,记录区块哈希与节点版本,确保后续任何人重放能得到相同结果。
  2. 工具版本记录:聚类规则、标签库版本、脚本 commit 都要写进报告,否则结论无法复现。
  3. 证据链:每一笔关键转账都要给出 tx hash、区块号、时间戳、金额与涉及地址,形成不可篡改的引用链。
  4. 置信度标注:区分「链上事实」(可直接验证)与「推断结论」(启发式得出)。混币器下游地址的归属往往只能给出概率。
  5. 合规口径:制裁名单(OFAC SDN)、FATF 旅行规则、以及各国监管要求的口径差异要显式说明。

一份合格的取证报告结构:

一、案件背景与调查范围
二、数据来源与工具说明(节点、区块高度、工具版本)
三、涉案地址与实体归属(附聚类依据与置信度)
四、资金流向时间线(逐笔列明 tx hash)
五、跨链与混币环节的处理说明
六、结论与合规建议(是否触发上报义务)
附录:完整地址清单与可复现脚本

与监管框架的衔接可参考 区块链监管与合规 ,其中对旅行规则与托管责任的讨论直接决定了取证报告要覆盖哪些字段。

稳定币、NFT 与出金环节的追踪

资金最终要变现才算完成洗钱闭环,而变现环节恰恰是最容易被截断的地方。

稳定币的特殊性

稳定币(USDT、USDC)发行方具备中心化冻结能力。这改变了取证策略:一旦确认涉案资金流入受制裁地址或已知犯罪地址,可以向发行方申请冻结,而不必完整追踪到法币出金。实务流程是「先冻结、再追踪」,因为冻结窗口稍纵即逝。

技术上的追踪要点:

  • 稳定币转账同样有内部调用,必须解析合约事件而非只看 value。
  • 很多稳定币在波场等非 EVM 链上发行量更大,取证范围要覆盖多条链。
  • 跨链稳定币(如 USDC 的 CCTP)在源链销毁、目标链铸造,追踪要用到跨链配对。

NFT 洗盘(Wash Trading)

NFT 是洗钱的新通道,因为价格高度主观。典型手法是自买自卖抬价:

def detect_wash_trade(sales, addr_clusters, price_multiple=5):
    """识别疑似洗盘:买卖双方属同一聚类且价格显著偏离"""
    alerts = []
    for s in sales:
        buyer_cluster = addr_clusters.get(s.buyer)
        seller_cluster = addr_clusters.get(s.seller)
        if buyer_cluster and buyer_cluster == seller_cluster:
            alerts.append({
                "tx": s.tx_hash,
                "token_id": s.token_id,
                "price": s.price,
                "cluster": buyer_cluster,
                "reason": "self_trade",
            })
        elif s.price > s.collection_floor * price_multiple:
            alerts.append({
                "tx": s.tx_hash, "price": s.price,
                "floor": s.collection_floor, "reason": "price_spike",
            })
    return alerts

法币出金环节

这是链上数据的边界。交易所的 KYC 数据在链下,执法方需要走法律程序调取。取证报告在此处的价值是提供精确的入金地址与时间,让交易所能在自己的数据库中定位到具体账户。

需要提前准备的信息:

  • 涉案资金进入交易所充值地址的精确 tx hash 与时间。
  • 该充值地址所属交易所的归集路径(证明资金确实进入了交易所体系)。
  • 若资金经过混币,说明「无法直接关联」的范围与理由。

合规上报的触发条件与各国要求差异很大,同一笔资金流在不同司法辖区可能触发完全不同的义务,取证报告必须显式声明适用的监管口径。

常见误区与排错

误区后果正确做法
只看外部交易不看内部转账漏掉合约内部的资金转移用 trace API 拉完整调用树
把聚类结果当事实错误指控无关地址标注置信度,保留反证空间
忽略 reorg证据链指向被回滚的交易只采用深度确认后的区块
用最新标签追溯历史实体归属错乱标签带时间戳,按案发窗口取值
忽略 gas 出资漏掉串联地址的关键线索作为辅助证据而非主证据
手工查浏览器无法规模化且不可复现脚本化 + 数据固化

最后一条实践建议:任何取证脚本都要能在离线数据集上重跑。把 RPC 查询结果落盘成结构化文件(parquet 或 sqlite),分析阶段只读本地数据,既保证可复现,也避免反复打归档节点。对于托管与多签场景下的资金归属判定,还需了解密钥分片方案带来的签名特征差异(例如 MPC 钱包的签名 nonce 复用模式与普通 EOA 明显不同),避免把托管钱包误判为个人地址。

小结

链上取证的难点从来不是「数据不够」,而是「数据太多且充满噪声」。有效的取证依赖三件事:以归档全节点为事实基准、用带置信度的启发式做聚类、把结论固化成可复现的证据链。技术只是工具,最终能站得住的仍然是对每一笔转账都能给出 tx hash 的逐笔举证。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. DePIN 去中心化物理基础设施网络
  2. DeFi 衍生品:期权、永续合约与合成资产
  3. 智能合约形式化验证:Certora 与 K 框架