← 返回首页
English Version

TRC20-USDT 链上证据固定实验

摘要

虚拟货币资金链溯源的起点通常是一笔链上交易。对于 TRC20-USDT 交易而言,交易哈希、转出地址、转入地址、Token 合约、时间戳和金额等字段均可通过链上数据源获得。但在取证实践中,仅保存区块链浏览器截图并不能满足后续复核、结构化分析和完整性校验的需要。本文以一条公开 USDT-TRC20 转账样例为实验对象,设计并实现了一个最小链上证据包生成流程。

关键词:Forensics · OnChain · TRC20-USDT

#1. 研究背景

虚拟货币洗钱案件中,资金流转不再完全依赖传统银行账户体系。链上地址匿名化、跨境交易、场外兑换、混币与跨链等因素,使传统“查流水—冻账户—找人员”的路径受到限制。此前研究汇报中已经将虚拟货币洗钱侦查难点概括为资金链断裂、身份难落地和跨境协作弱,并提出需要建立涵盖线索发现、地址追踪、图谱分析、身份落地、证据固定的标准化溯源操作框架。

本文只取其中一个环节:证据固定

链上交易公开可查,但“可查”并不等于“已经完成取证”。如果只保存浏览器截图,至少存在四个问题:第一,截图字段不一定完整;第二,截图无法直接参与程序化复核;第三,页面展示可能受浏览器、语言、缓存和平台改版影响;第四,截图文件本身缺少完整性校验。因此,单笔 TRC20-USDT 交易应当被整理成结构化证据包,而不是停留在网页截图层面。


#2. 数据来源与实验对象

TRONGrid 官方文档提供了按账户查询指定 TRC20 Token 交易历史的接口,并在示例中使用 contract_address=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t 查询 TRC20-USDT 交易记录。该接口支持 limitfingerprintcontract_address 等参数,其中 contract_address 用于限定 TRC20 合约地址。([TRON Developer Hub](https://developers.tron.network/docs/get-trc20-transaction-history "Get TRC-20 Transaction History"))

本实验使用一条公开 USDT-TRC20 transaction-history 返回样例作为固定输入。该样例字段包括 transaction_idtoken_infoblock_timestampfromtotypevalue,其中 Token 合约地址为 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t,Token 为 Tether USD,精度为 6。公开样例来自 USDT-TRC20 查询接口说明中的返回示例。([GitHub](https://github.com/dogpig4311/tron-api-code-trc20-trx-trc10-PHP/blob/main/README-EN.md "tron-api-code-trc20-trx-trc10-PHP/README-EN.md at main · dogpig4311/tron-api-code-trc20-trx-trc10-PHP · GitHub"))

TRON 官方交易文档说明,TRON 交易是由账户发起并签名的链上状态变更,交易被打包进区块并确认后才最终确认;同时,raw_data.timestamp 是交易创建时间,而包含交易的区块时间才是实际链上时间。该点说明,在证据固定中应区分“交易字段时间”和“链上区块时间”。([TRON Developer Hub](https://developers.tron.network/docs/tron-protocol-transaction "Transactions"))

本实验样本如下:

json
{
  "transaction_id": "d52cd9079cf82595dd507640b7b09e34d2dbb63a56b555355f5ef8984f1eb668",
  "token_info": {
    "symbol": "USDT",
    "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "decimals": 6,
    "name": "Tether USD"
  },
  "block_timestamp": 1651903617000,
  "from": "TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny",
  "to": "TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss",
  "type": "Transfer",
  "value": "15500000"
}

需要说明的是,该公开样例不包含区块高度字段,因此本文实验结果中区块高度记为 null,不作补填。


#3. 方法设计

#3.1 最小链上证据包

本文将单笔 USDT-TRC20 交易固定为以下文件集合:

text
usdt_trc20_evidence_experiment/
├── 01_basic_info.json
├── 02_raw_usdt_trc20_sample.json
├── 03_transfer_event.csv
├── 04_timeline.csv
├── 05_relation_edges.csv
├── 06_relation_graph.dot
├── 07_collection_log.txt
├── 08_hash_manifest.txt
├── README.md
└── build_evidence_package.py

其中:

01_basic_info.json 保存规范化后的核心字段。 02_raw_usdt_trc20_sample.json 保存原始样例数据。 03_transfer_event.csv 保存单笔 Transfer 事件表。 04_timeline.csv 保存时间线。 05_relation_edges.csv 保存关系边。 06_relation_graph.dot 保存 Graphviz 关系图。 07_collection_log.txt 保存采集与生成日志。 08_hash_manifest.txt 保存所有文件的 SHA256。 build_evidence_package.py 为可复现实验代码。

#3.2 字段规范化

实验对金额字段进行了精度换算。样例中的 value15500000,Token 精度 decimals 为 6,因此实际金额为:

text
15500000 / 10^6 = 15.5 USDT

时间戳字段 block_timestamp 为毫秒级 Unix 时间戳:

text
1651903617000

换算为 UTC 时间:

text
2022-05-07T06:06:57+00:00

#4. 实验结果

#4.1 基础字段提取结果

本地实验实际生成的 01_basic_info.json 核心结果如下:

json
{
  "experiment": "USDT-TRC20 single-transfer evidence-package fixation",
  "chain": "TRON",
  "token_standard": "TRC20",
  "token_name": "Tether USD",
  "token_symbol": "USDT",
  "contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
  "txid": "d52cd9079cf82595dd507640b7b09e34d2dbb63a56b555355f5ef8984f1eb668",
  "block": null,
  "block_timestamp_ms": 1651903617000,
  "block_time_utc": "2022-05-07T06:06:57+00:00",
  "from": "TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny",
  "to": "TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss",
  "amount_raw": "15500000",
  "decimals": 6,
  "amount_decimal": "15.5",
  "event_type": "Transfer"
}

#4.2 交易事件表

生成的 03_transfer_event.csv 内容如下:

csv
txid,block,block_time_utc,from,to,amount_raw,decimals,amount_decimal,token_symbol,contract_address,event_type
d52cd9079cf82595dd507640b7b09e34d2dbb63a56b555355f5ef8984f1eb668,,2022-05-07T06:06:57+00:00,TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny,TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss,15500000,6,15.5,USDT,TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t,Transfer

#4.3 时间线

生成的 04_timeline.csv 内容如下:

csv
order,type,txid,block_time_utc,from,to,amount_decimal,note
1,target_transfer,d52cd9079cf82595dd507640b7b09e34d2dbb63a56b555355f5ef8984f1eb668,2022-05-07T06:06:57+00:00,TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny,TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss,15.5,Single USDT-TRC20 Transfer event fixed as target object

#4.4 关系边

生成的 05_relation_edges.csv 内容如下:

csv
source,target,txid,block_time_utc,amount_decimal,token_symbol
TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny,TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss,d52cd9079cf82595dd507640b7b09e34d2dbb63a56b555355f5ef8984f1eb668,2022-05-07T06:06:57+00:00,15.5,USDT

#4.5 Graphviz 关系图

生成的 06_relation_graph.dot 内容如下:

dot
digraph USDT_TRC20_Evidence {
  graph [rankdir=LR];
  node [shape=box, fontname="Arial"];
  "TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny" [label="FROM\nTYPrKF2s...cybny"];
  "TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss" [label="TO\nTTRmEA73...VefYss"];
  "TYPrKF2sevXuE86Xo3Y2mhFnjseiUcybny" -> "TTRmEA73gpoxK2KRmhL7GtcYLh88VefYss" [label="15.5 USDT\n2022-05-07T06:06:57+00:00"];
}

该图只表达最小资金方向:

text
TYPr...cybny  →  TTRm...VefYss
        15.5 USDT

#4.6 哈希清单

生成的 08_hash_manifest.txt 内容如下:

text
7fe8aa03d9b86e1f32f4c1cdd131e6718cc41effd43d4fa0eeed5c5badea57a7  01_basic_info.json
b4522f4058e7aebde624f8e43c3e5c20cf14cebe4b9e35b04e07e861092c3df7  02_raw_usdt_trc20_sample.json
a622c54d7794cc15bf72e4db6a7e59b9afadce64f87d8c4585d3963c12b4b303  03_transfer_event.csv
15c731d2ec8228b14aaef436087441fdf0b35ce5a0c1b20369b0cb7bb4f479ec  04_timeline.csv
abff190982b09be8b90c26b342291d392c06868f330233ed5f5aae4f9d5323ef  05_relation_edges.csv
c1aa19fc6eeef16c89257e34d7ad580cf2602f9e0369cc469514de270fc58c29  06_relation_graph.dot
9347aeafc136f2158517e33e9ed57416efee8aa37c2ee8628cda24eab2bf9dfb  07_collection_log.txt
a2648a96b883b1e8726a049decb23169cbf328a013b3b0a0aa9ad67a82f2e8a8  README.md
87a584838c8b01c8241082aeb2c6ce13bd3d37303cb42eadea1214de236901be  build_evidence_package.py

哈希清单的作用是保证证据包文件在生成后具备完整性校验基础。后续若任一文件被修改,重新计算 SHA256 即可发现变化。


#5. 实验代码

完整代码已放入实验包中的 build_evidence_package.py。核心逻辑如下:

python
from pathlib import Path
from decimal import Decimal, getcontext
import json, csv, hashlib, datetime, sys, shutil

getcontext().prec = 50

def sha256_file(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()

def main(raw_json: str, out_dir: str) -> None:
    raw_path = Path(raw_json)
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)

    data = json.loads(raw_path.read_text(encoding="utf-8"))
    event = data["data"][0]
    token = event["token_info"]

    shutil.copy2(raw_path, out / "02_raw_usdt_trc20_sample.json")

    amount_decimal = Decimal(event["value"]) / (
        Decimal(10) ** int(token["decimals"])
    )
    block_time_utc = datetime.datetime.fromtimestamp(
        event["block_timestamp"] / 1000,
        tz=datetime.timezone.utc
    )

    basic = {
        "experiment": "USDT-TRC20 single-transfer evidence-package fixation",
        "chain": "TRON",
        "token_standard": "TRC20",
        "token_name": token.get("name"),
        "token_symbol": token.get("symbol"),
        "contract_address": token.get("address"),
        "txid": event.get("transaction_id"),
        "block_timestamp_ms": event.get("block_timestamp"),
        "block_time_utc": block_time_utc.isoformat(),
        "from": event.get("from"),
        "to": event.get("to"),
        "amount_raw": event.get("value"),
        "decimals": token.get("decimals"),
        "amount_decimal": str(amount_decimal),
        "event_type": event.get("type"),
        "generated_at_utc": datetime.datetime.now(datetime.timezone.utc).isoformat()
    }

    (out / "01_basic_info.json").write_text(
        json.dumps(basic, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )

    with (out / "03_transfer_event.csv").open(
        "w", encoding="utf-8", newline=""
    ) as f:
        writer = csv.DictWriter(f, fieldnames=[
            "txid", "block_time_utc", "from", "to", "amount_raw",
            "decimals", "amount_decimal", "token_symbol",
            "contract_address", "event_type"
        ])
        writer.writeheader()
        writer.writerow({
            "txid": event.get("transaction_id"),
            "block_time_utc": block_time_utc.isoformat(),
            "from": event.get("from"),
            "to": event.get("to"),
            "amount_raw": event.get("value"),
            "decimals": token.get("decimals"),
            "amount_decimal": str(amount_decimal),
            "token_symbol": token.get("symbol"),
            "contract_address": token.get("address"),
            "event_type": event.get("type")
        })

    with (out / "04_timeline.csv").open(
        "w", encoding="utf-8", newline=""
    ) as f:
        writer = csv.DictWriter(f, fieldnames=[
            "order", "type", "txid", "block_time_utc",
            "from", "to", "amount_decimal", "note"
        ])
        writer.writeheader()
        writer.writerow({
            "order": 1,
            "type": "target_transfer",
            "txid": event.get("transaction_id"),
            "block_time_utc": block_time_utc.isoformat(),
            "from": event.get("from"),
            "to": event.get("to"),
            "amount_decimal": str(amount_decimal),
            "note": "Single USDT-TRC20 Transfer event fixed as target object"
        })

    with (out / "05_relation_edges.csv").open(
        "w", encoding="utf-8", newline=""
    ) as f:
        writer = csv.DictWriter(f, fieldnames=[
            "source", "target", "txid",
            "block_time_utc", "amount_decimal", "token_symbol"
        ])
        writer.writeheader()
        writer.writerow({
            "source": event.get("from"),
            "target": event.get("to"),
            "txid": event.get("transaction_id"),
            "block_time_utc": block_time_utc.isoformat(),
            "amount_decimal": str(amount_decimal),
            "token_symbol": token.get("symbol")
        })

    from_addr = event.get("from")
    to_addr = event.get("to")

    dot = f"""digraph USDT_TRC20_Evidence {{
  graph [rankdir=LR];
  node [shape=box, fontname="Arial"];
  "{from_addr}" [label="FROM\\\\n{from_addr[:8]}...{from_addr[-6:]}"];
  "{to_addr}" [label="TO\\\\n{to_addr[:8]}...{to_addr[-6:]}"];
  "{from_addr}" -> "{to_addr}" [label="{amount_decimal} {token.get("symbol")}\\\\n{block_time_utc.isoformat()}"];
}}
"""
    (out / "06_relation_graph.dot").write_text(dot, encoding="utf-8")

    rows = []
    for p in sorted(out.iterdir()):
        if p.name == "08_hash_manifest.txt" or p.is_dir():
            continue
        rows.append(f"{sha256_file(p)}  {p.name}")

    (out / "08_hash_manifest.txt").write_text(
        "\n".join(rows) + "\n",
        encoding="utf-8"
    )

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(
            "Usage: python build_evidence_package.py raw_usdt_trc20_sample.json output_dir",
            file=sys.stderr
        )
        sys.exit(2)

    main(sys.argv[1], sys.argv[2])

运行方式:

bash
python build_evidence_package.py 02_raw_usdt_trc20_sample.json output_dir

#6. 讨论

#6.1 单笔交易固定的价值

本实验没有试图直接追完整资金链,而是只处理一笔 USDT-TRC20 转账。这样做的意义在于:资金链溯源的每一步都建立在单笔交易固定之上。如果单笔交易字段不完整,后续的地址扩展、路径分析、图谱可视化和报告撰写都会失去稳定基础。

#6.2 原始数据不能被字段提取结果替代

01_basic_info.json 是规范化结果,便于阅读;02_raw_usdt_trc20_sample.json 是原始样例数据,便于复核。两者不能互相替代。取证时应先保存原始数据,再从原始数据中提取字段。

#6.3 样例字段存在局限

本实验所用公开样例不包含区块高度字段,因此实验结果没有补写区块高度。这一点很重要:证据固定不应为了“好看”而补造字段。缺失就是缺失,应在结果中说明。

#6.4 链上交易不能直接证明真实身份

该证据包只能证明某一时间发生了从地址 A 到地址 B 的 USDT-TRC20 转账。它不能直接证明地址背后的实际控制人,也不能证明资金性质。身份落地仍需要交易所账户、KYC、登录 IP、设备痕迹、聊天记录、银行卡流水等链下材料支撑。


#7. 结论

本文围绕“一笔 USDT-TRC20 交易如何被取证”进行了本地实验。实验基于公开 USDT-TRC20 返回样例,完成了原始数据保存、字段规范化、金额换算、时间戳换算、事件表生成、时间线生成、关系边生成、DOT 图生成和 SHA256 完整性校验。

实验结果表明,单笔链上交易可以被整理为一个最小证据包。该证据包不能替代完整案件证据,但可以为后续资金链溯源提供可靠起点。相比单纯截图,结构化证据包更适合复核、比对、扩展和报告撰写。

因此,在虚拟货币资金链溯源中,交易哈希不应只是查询入口,而应成为链上证据固定流程的起点。链上取证的第一步,不是画复杂资金图谱,而是把第一笔交易固定清楚。