TRC20-USDT On-Chain Evidence Preservation Experiment
Abstract
The starting point for tracing virtual currency fund flows is usually an on-chain transaction. For TRC20-USDT transactions, fields such as the transaction hash, sender address, recipient address, token contract, timestamp, and amount can all be obtained from on-chain data sources. However, in forensic practice, merely preserving screenshots from blockchain explorers is insufficient for subsequent review, structured analysis, and integrity verification. Taking a public USDT-TRC20 transfer as the experimental object, this paper designs and implements a minimal workflow for generating an on-chain evidence package.
Here is the English translation based on the uploaded markdown file.
#1. Research Background
In virtual currency money laundering cases, fund flows no longer rely entirely on the traditional bank account system. Factors such as the anonymity of on-chain addresses, cross-border transactions, over-the-counter exchanges, mixing services, and cross-chain transfers have limited the traditional investigative path of “checking bank statements — freezing accounts — identifying individuals.” In the previous research presentation, the difficulties of investigating virtual currency money laundering were summarized as broken fund chains, difficulty in linking identities to addresses, and weak cross-border cooperation. It was also proposed that a standardized tracing framework should be established, covering clue discovery, address tracking, graph analysis, identity attribution, and evidence preservation.
This paper focuses on only one part of that process: evidence preservation.
On-chain transactions are publicly searchable, but “searchable” does not mean “forensically preserved.” If only blockchain explorer screenshots are saved, at least four problems arise. First, the fields shown in screenshots may be incomplete. Second, screenshots cannot directly support programmatic review. Third, page displays may be affected by browser settings, language, cache, and platform redesigns. Fourth, screenshot files themselves lack integrity verification. Therefore, a single TRC20-USDT transaction should be organized into a structured evidence package rather than remaining merely as webpage screenshots.
#2. Data Source and Experimental Object
The TRONGrid official documentation provides an API for querying the transaction history of a specified TRC20 token by account. In its example, contract_address=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t is used to query TRC20-USDT transaction records. The API supports parameters such as limit, fingerprint, and contract_address, where contract_address is used to specify the TRC20 contract address. ([TRON Developer Hub](https://developers.tron.network/docs/get-trc20-transaction-history "Get TRC-20 Transaction History"))
This experiment uses a public USDT-TRC20 transaction-history response sample as the fixed input. The sample fields include transaction_id, token_info, block_timestamp, from, to, type, and value. The token contract address is TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, the token is Tether USD, and the decimal precision is 6. The public sample comes from the return example in the USDT-TRC20 query API documentation. ([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"))
The official TRON transaction documentation explains that a TRON transaction is an on-chain state change initiated and signed by an account. A transaction is finalized only after it is packed into a block and confirmed. Meanwhile, raw_data.timestamp refers to the transaction creation time, while the timestamp of the block containing the transaction is the actual on-chain time. This indicates that, during evidence preservation, the “transaction field time” and the “on-chain block time” should be distinguished. ([TRON Developer Hub](https://developers.tron.network/docs/tron-protocol-transaction "Transactions"))
The experimental sample is as follows:
{
"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"
}It should be noted that this public sample does not include a block height field. Therefore, in the experimental results of this paper, the block height is recorded as null and is not artificially supplemented.
#3. Method Design
#3.1 Minimal On-Chain Evidence Package
This paper preserves a single USDT-TRC20 transaction as the following file set:
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.pyAmong them:
01_basic_info.json stores the normalized core fields. 02_raw_usdt_trc20_sample.json stores the original sample data. 03_transfer_event.csv stores the single Transfer event table. 04_timeline.csv stores the timeline. 05_relation_edges.csv stores the relationship edge. 06_relation_graph.dot stores the Graphviz relationship graph. 07_collection_log.txt stores the collection and generation log. 08_hash_manifest.txt stores the SHA256 hashes of all files. build_evidence_package.py is the reproducible experimental code.
#3.2 Field Normalization
The experiment performs precision conversion on the amount field. In the sample, value is 15500000, and the token precision decimals is 6. Therefore, the actual amount is:
15500000 / 10^6 = 15.5 USDTThe timestamp field block_timestamp is a millisecond-level Unix timestamp:
1651903617000Converted to UTC time:
2022-05-07T06:06:57+00:00#4. Experimental Results
#4.1 Basic Field Extraction Result
The core result of the locally generated 01_basic_info.json is as follows:
{
"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 Transaction Event Table
The generated 03_transfer_event.csv is as follows:
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 Timeline
The generated 04_timeline.csv is as follows:
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 Relationship Edge
The generated 05_relation_edges.csv is as follows:
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 Relationship Graph
The generated 06_relation_graph.dot is as follows:
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"];
}This graph only represents the minimal fund direction:
TYPr...cybny → TTRm...VefYss
15.5 USDT#4.6 Hash Manifest
The generated 08_hash_manifest.txt is as follows:
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.pyThe purpose of the hash manifest is to provide a basis for integrity verification after the evidence package is generated. If any file is modified later, the change can be detected by recalculating its SHA256 hash.
#5. Experimental Code
The complete code has been placed in build_evidence_package.py inside the experimental package. The core logic is as follows:
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])Run the script as follows:
python build_evidence_package.py 02_raw_usdt_trc20_sample.json output_dir#6. Discussion
#6.1 Value of Preserving a Single Transaction
This experiment does not attempt to trace a complete fund chain directly. Instead, it only processes one USDT-TRC20 transfer. The significance of this approach is that every step of fund-chain tracing is built upon the preservation of individual transactions. If the fields of a single transaction are incomplete, subsequent address expansion, path analysis, graph visualization, and report writing will all lose a stable foundation.
#6.2 Raw Data Cannot Be Replaced by Extracted Fields
01_basic_info.json is the normalized result and is easier to read. 02_raw_usdt_trc20_sample.json is the original sample data and is used for review. The two cannot replace each other. During forensic work, the original data should be preserved first, and then fields should be extracted from that original data.
#6.3 Limitations of the Sample Fields
The public sample used in this experiment does not include a block height field. Therefore, the experiment does not supplement a block height in the results. This point is important: evidence preservation should not fabricate fields merely to make the results look complete. Missing information should remain marked as missing and be clearly explained in the results.
#6.4 On-Chain Transactions Cannot Directly Prove Real-World Identity
This evidence package can only prove that, at a certain time, a USDT-TRC20 transfer occurred from address A to address B. It cannot directly prove the real controller behind the address, nor can it prove the nature of the funds. Identity attribution still requires off-chain materials such as exchange accounts, KYC records, login IP addresses, device traces, chat records, and bank transaction records.
#7. Conclusion
This paper conducts a local experiment on how a single USDT-TRC20 transaction can be forensically preserved. Based on a public USDT-TRC20 response sample, the experiment completes raw data preservation, field normalization, amount conversion, timestamp conversion, event table generation, timeline generation, relationship edge generation, DOT graph generation, and SHA256 integrity verification.
The experimental results show that a single on-chain transaction can be organized into a minimal evidence package. This evidence package cannot replace complete case evidence, but it can provide a reliable starting point for subsequent fund-chain tracing. Compared with simple screenshots, a structured evidence package is more suitable for review, comparison, expansion, and report writing.
Therefore, in virtual currency fund-chain tracing, a transaction hash should not be treated merely as a query entry point. It should become the starting point of the on-chain evidence preservation process. The first step of on-chain forensics is not to draw a complex fund graph, but to preserve the first transaction clearly.