TRC20-USDT 链上证据固定实验
摘要
虚拟货币资金链溯源的起点通常是一笔链上交易。对于 TRC20-USDT 交易而言,交易哈希、转出地址、转入地址、Token 合约、时间戳和金额等字段均可通过链上数据源获得。但在取证实践中,仅保存区块链浏览器截图并不能满足后续复核、结构化分析和完整性校验的需要。本文以一条公开 USDT-TRC20 转账样例为实验对象,设计并实现了一个最小链上证据包生成流程。
#1. 研究背景
虚拟货币洗钱案件中,资金流转不再完全依赖传统银行账户体系。链上地址匿名化、跨境交易、场外兑换、混币与跨链等因素,使传统“查流水—冻账户—找人员”的路径受到限制。此前研究汇报中已经将虚拟货币洗钱侦查难点概括为资金链断裂、身份难落地和跨境协作弱,并提出需要建立涵盖线索发现、地址追踪、图谱分析、身份落地、证据固定的标准化溯源操作框架。
本文只取其中一个环节:证据固定。
链上交易公开可查,但“可查”并不等于“已经完成取证”。如果只保存浏览器截图,至少存在四个问题:第一,截图字段不一定完整;第二,截图无法直接参与程序化复核;第三,页面展示可能受浏览器、语言、缓存和平台改版影响;第四,截图文件本身缺少完整性校验。因此,单笔 TRC20-USDT 交易应当被整理成结构化证据包,而不是停留在网页截图层面。
#2. 数据来源与实验对象
TRONGrid 官方文档提供了按账户查询指定 TRC20 Token 交易历史的接口,并在示例中使用 contract_address=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t 查询 TRC20-USDT 交易记录。该接口支持 limit、fingerprint 和 contract_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_id、token_info、block_timestamp、from、to、type 和 value,其中 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"))
本实验样本如下:
{
"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 交易固定为以下文件集合:
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 字段规范化
实验对金额字段进行了精度换算。样例中的 value 为 15500000,Token 精度 decimals 为 6,因此实际金额为:
15500000 / 10^6 = 15.5 USDT时间戳字段 block_timestamp 为毫秒级 Unix 时间戳:
1651903617000换算为 UTC 时间:
2022-05-07T06:06:57+00:00#4. 实验结果
#4.1 基础字段提取结果
本地实验实际生成的 01_basic_info.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 内容如下:
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 内容如下:
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 内容如下:
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 内容如下:
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"];
}该图只表达最小资金方向:
TYPr...cybny → TTRm...VefYss
15.5 USDT#4.6 哈希清单
生成的 08_hash_manifest.txt 内容如下:
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。核心逻辑如下:
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])运行方式:
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 完整性校验。
实验结果表明,单笔链上交易可以被整理为一个最小证据包。该证据包不能替代完整案件证据,但可以为后续资金链溯源提供可靠起点。相比单纯截图,结构化证据包更适合复核、比对、扩展和报告撰写。
因此,在虚拟货币资金链溯源中,交易哈希不应只是查询入口,而应成为链上证据固定流程的起点。链上取证的第一步,不是画复杂资金图谱,而是把第一笔交易固定清楚。