← Home
中文 Version

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.

Keywords:Forensics · OnChain · TRC20-USDT

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:

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"
}

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:

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

Among 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:

text
15500000 / 10^6 = 15.5 USDT

The timestamp field block_timestamp is a millisecond-level Unix timestamp:

text
1651903617000

Converted to UTC time:

text
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:

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 Transaction Event Table

The generated 03_transfer_event.csv is as follows:

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 Timeline

The generated 04_timeline.csv is as follows:

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 Relationship Edge

The generated 05_relation_edges.csv is as follows:

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 Relationship Graph

The generated 06_relation_graph.dot is as follows:

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"];
}

This graph only represents the minimal fund direction:

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

#4.6 Hash Manifest

The generated 08_hash_manifest.txt is as follows:

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

The 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:

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])

Run the script as follows:

bash
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.