博客
10 分钟阅读
|
2026年7月04日

什么是 TRC20 地址?

什么是 TRC20 地址、为什么它以字母 T 开头、hex 与 Base58 两种编码形式的区别、合约地址与 wallet 地址有何不同,以及如何通过 REST API 批量创建并注册 TRON 地址,本文为你一次性讲清楚。

章节
C
Chaingateway Team
区块链专家

TRC20 地址是用于发送和接收 TRC20 token 的 TRON 账户地址,最常见的就是 USDT。它以字母 T 开头,长度为 34 个字符,例如 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t。严格来说并不存在单独的「TRC20 地址类型」:任何一个 TRON 地址都可以同时持有 TRX、TRC10 token 和 TRC20 token。当交易所要求你提供「USDT TRC20 地址」时,指的就是你的普通 TRON 地址。

这个格式和你在 Ethereum 或 BSC 上熟悉的 0x 地址完全不同,而且 TRON 还带来一个最让开发者困惑的细节:同一个地址还有第二种、以 41 开头的十六进制写法。本文将依次讲解编码方式、hex 与 Base58 之争、wallet 地址与合约地址的区别、验证代码,以及如何通过 API 大规模创建 TRON 地址。

格式:T 加 33 个字符,Base58Check

TRON 地址的人类可读形式采用 Base58Check 编码。Base58 与 Bitcoin 使用的是同一套字母表:除了容易在印刷中混淆的 0OIl 之外的所有数字和字母。因此,一个有效的 TRON 地址:

  • T 开头
  • 长度恰好为 34 个字符
  • 只包含 Base58 字母表中的字符

「Check」这部分意味着这种编码自带一个内置的校验和。在 Base58 表层之下,一个地址由 25 字节组成:前缀字节 0x41、20 字节的账户标识符,以及 4 字节的校验和。一个字符打错就会破坏校验和,因此 wallet 可以在任何交易被签名之前拒绝这个打字错误。这一点很重要,因为与 Ethereum 那种可选的 EIP-55 大小写校验不同,TRON 地址中的校验和不是可选项。每一个有效地址都必然带有校验和。

TRC-20 与 ERC-20 与 BEP-20 对比

USDT 同时存在于这三个网络上,因此这些格式每天都会在提现表单和支持工单中相遇。截至 2026 年年中,区分它们的关键数字如下:

TRC-20(TRON)ERC-20(Ethereum)BEP-20(BNB Smart Chain)
地址格式T + 33 个 Base58 字符0x + 40 个 hex 字符0x + 40 个 hex 字符
校验和Base58Check,始终存在EIP-55 大小写校验,可选EIP-55 大小写校验,可选
与其他格式混淆的风险无,格式本身无法通过验证高:与 BEP-20 字符串完全相同高:与 ERC-20 字符串完全相同
block time3 秒12 秒0.45 秒
最终确认所需时间约 57 秒(19 个 block)约 13–16 分钟(两个 epoch)1–2 秒
典型 USDT 转账手续费未质押 energy 时约 6.4–13.4 TRX几十美分(以 ETH 计),高峰期更高几美分(以 BNB 计)

中间几行解释了为什么这篇文章一直在提醒你警惕 EVM 系 chain,而不是 TRON 本身。ERC-20 地址和 BEP-20 地址是完全相同的字符串,地址本身丝毫无法告诉发送方或验证方到底指的是哪条 chain;而 TRON 地址永远不会与两者中的任何一个混淆。TRON 用户真正会犯的错误其实出现在手续费和网络这两行:在交易所的下拉菜单里选错网络,或者低估了把存款继续转出去要花多少钱。

时间相关的几行值得标注一个具体日期。BSC 在 2026 年 1 月的 Fermi 硬分叉中把 block time 降到了 0.45 秒,这是一年内第二次缩短 block time;而 TRON 自上线以来一直保持每 3 秒出一个 block,Ethereum 自转向 Proof of Stake 以来则一直保持 12 秒的时间槽。TRON 19 个 block、约 57 秒的最终确认窗口,正是大多数交易所在为 TRC20 存款入账前所等待的时间,这也是为什么即便一切正常,「已发送」和「已入账」之间也会相差大约一分钟。

TRON 地址是如何生成的

这个生成过程比结果的外观所暗示的更接近 Ethereum:

  1. 在 secp256k1 曲线上生成一个私钥,这与 Bitcoin 和 Ethereum 使用的是同一条曲线。
  2. 计算出公钥,并用 Keccak-256 对其做 hash。
  3. 保留 hash 的最后 20 字节。到这一步为止,流程与 Ethereum 完全一致。
  4. 在前面加上字节 0x41(TRON 的 mainnet 前缀),得到 21 字节。
  5. 用 SHA-256 对这 21 字节做两次 hash,取前 4 字节作为校验和。
  6. 把校验和拼接上去,并将这 25 字节用 Base58 编码。

开头的 T 并不是谁为了品牌效果特意挑选的约定,它完全是计算的结果。任何以 0x41 开头的 25 字节字符串,编码成 Base58 文本后都会以 T 开头。

用真实数字演示 Base58Check

编码方式如果能自己动手复现一遍会更容易记住,下面就用 USDT 合约地址,把它从各个组成部分拼出来。这个 21 字节的 payload 就是前缀 41 加上 20 字节的账户标识符:

payload: 41a614f803b6fd780986a42c78ec9c7f77e6ded13c
sha256(payload): 3a42512dd4f64e4d9dad3d5e6aa0ecf55adbd8a85979135cf211ba347278d029
sha256(again): 710277f5d80b170d90e2ec6c52caa368bc74b9226e3befe5c09825a0eedbfb68
checksum: 710277f5

校验和就是第二次 hash 的前 4 字节。把它拼接到 payload 后面,你就得到了 25 字节:41a614f803b6fd780986a42c78ec9c7f77e6ded13c710277f5。把这些字节当成一个大数字,反复除以 58,再把每一次的余数映射到 Base58 字母表上。得到的结果就是 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t,也就是每个 wallet 显示出来的那个地址。

解码则是反向走同一条路:从 Base58 得到 25 字节,分离出最后 4 字节,对前 21 字节用 SHA-256 做两次 hash,然后比较。如果比较失败,说明某个字符在传输过程中被改动了,这个地址就不能使用。正是这个校验机制,让任何靠谱的 wallet 都能拒绝一个打错的 TRON 地址:一次随机的打字错误恰好还能产生匹配的 4 字节校验和的概率是 2^32 分之一,大约是四十亿分之一。相比之下,Ethereum 的 EIP-55 大约只用 15 个校验位来捕捉打字错误,所以 TRON 的方案要严格得多。

Hex 形式(41…)与 Base58 形式(T…):同一个地址,两种写法

接下来是几乎没有任何科普文章讲到的部分。当你使用 TRON 自己的 node API 或底层库时,返回的地址会是以 41 开头的十六进制形式。以 USDT 的 token 合约为例:

Base58: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
Hex: 41a614f803b6fd780986a42c78ec9c7f77e6ded13c

两种写法指向的是同一个账户。hex 形式是原始的 21 字节(前缀加标识符),不带校验和;Base58 形式则是把这些字节连同校验和一起包装起来,方便人来使用。wallet 和区块浏览器显示的是 T...,而原始的 node 响应和已签名的交易 payload 使用的是 41...

这里有两个实际后果。第一,永远不要在不统一编码格式的情况下把地址当作字符串直接比较,否则 TR7N...41a6... 在你的代码看来会像是两个不同的账户。第二,如果把 hex 形式的 41 前缀去掉,剩下的 20 字节和 Ethereum 地址的形状完全一样,这也是为什么有些库能够在 TRON 和 EVM 两种表示之间互相转换。形状相同不代表账户相同:同一个 key 用在两条网络上会产生毫无关联的地址,因为从第 4 步开始,两者的生成方式就已经分道扬镳。

在两种形式之间转换

这个转换足够简短,完全可以自己写,不必特意引入一个 TRON SDK。从 Base58 到 hex 意味着解码并丢弃校验和;反方向则是重新计算校验和:

import bs58 from "bs58";
import { createHash } from "node:crypto";
const sha256 = (b) => createHash("sha256").update(b).digest();
function base58ToHex(address) {
const raw = Buffer.from(bs58.decode(address));
return raw.subarray(0, 21).toString("hex"); // 丢弃 4 字节校验和
}
function hexToBase58(hex) {
const payload = Buffer.from(hex, "hex"); // 21 字节,以 41 开头
const checksum = sha256(sha256(payload)).subarray(0, 4);
return bs58.encode(Buffer.concat([payload, checksum]));
}

存储时选定一种规范形式,只在边界处做转换。存 Base58 能让你的数据库和用户、浏览器看到的内容保持一致;存 hex 则和原始 node payload 中的内容一致。两种方式都可行,但如果在同一张表里把两者混用,「同一个地址却对不上」这个 bug 就是这么诞生的,而且通常是在凌晨两点对账存款时才被发现。

合约地址与 wallet 地址

wallet 和 smart contract 都生活在 T... 地址上,字符串本身丝毫无法把它们区分开。这个区别对 USDT 来说尤为重要:

  • 你的 wallet 地址是用来接收 USDT 的地方,它属于你的私钥。
  • TRC20 合约地址是 token 代码所在的地方。对于 TRON 上的 USDT 来说,那就是 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t

当你把自定义 token 添加到 wallet 中、在 Tronscan 上核实某个 token 是真正的 USDT 而不是同名的仿冒品,或是从代码中调用该 token 时,都会遇到合约地址。绝对不能做的一件事,是把 token 发送到 合约地址。token 合约没有可以把资金退回来的所有者,所以几乎所有情况下,发到合约上的转账都是永久丢失的。区块浏览器会给合约账户打上「Contract」标签,这是判断你面对的是哪种地址最快的方法。

在 Tronscan 上逐步核实一个地址

Tronscan 是 TRON 的区块浏览器,大多数疑问在一分钟之内就能解决。

把地址粘贴到搜索框里。账户页面会显示 TRX 余额、TRC20 持仓,以及每一笔转入转出。全新的存款地址会显示一个空页面,这很正常;TRON 账户是靠第一笔转入交易激活的,所以「暂无数据」意味着尚未使用,而不是地址无效。

接下来检查账户类型。合约账户会在页面顶部附近带有明显的「Contract」标签。资金应该发往普通账户;如果别人给你的目标地址被标记为合约,就不要发送。

对于 token,请按合约地址搜索,而不是按名称搜索。在 Tronscan 上输入「USDT」会返回真正的 token,周围还围绕着一堆使用相同 ticker 的仿冒品,而在 TRON 上任何人都可以部署一个叫 USDT 的 token。真正的那个位于 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t,精度为六位小数,它的 Tronscan 页面会显示发行方标签,持有人数量则达到数千万级别。如果某个陌生 token 的页面只显示几百个持有人,又没有经过验证的发行方,那它就不是你以为的那个资产。

每次转账之后的最后一步:搜索交易 ID。详情页面会列出发送方、接收方、token 合约、金额以及确认状态。一旦交易在 19 个 block 的窗口之后显示为已确认,它就是最终的。

不要搞混网络

USDT 同时存在于多条 chain 上:在 TRON 上是 TRC20 token,在 Ethereum 上是 ERC-20 token,在 BSC 上是 BEP-20 token。ticker 相同,合约各自独立,网络互不兼容。

在这一点上 TRON 其实是相对友好的一方,因为格式上有明显区别:T... 地址粘贴进 Ethereum 的提现表单里必然无法通过验证,而 0x... 地址在 TRON 上同样无法通过验证。真正的风险出现在交易所的提现菜单里,你需要从地址栏旁边的下拉菜单中选择网络。如果选择「ERC20」却粘贴了一个 TRON 地址,一个合格的交易所会拦截它。但如果你在多条网络上都持有地址,并且不小心粘贴了一个恰好与所选网络匹配的错误地址,任何验证机制都救不了你。在任何显示或接收地址的地方,都要明确标注网络。关于这个问题在 EVM 一侧的情况——同一个地址同时存在于多条 chain 上——可以参考什么是 BEP20 地址?

Address poisoning:校验和抓不住的攻击

前面提到的所有检查都只能防住无心之失。address poisoning 是蓄意为之的攻击,而且它之所以有效,恰恰是因为那个被投毒的地址完全合法。

攻击的设置是这样的:攻击者先观察你的 on-chain 活动,然后碾出一个 vanity 地址,让它的首尾字符与你经常打交道的某个地址相匹配。生成一个与目标地址首尾各匹配四个字符的 TRON 地址,只是计算时间的问题,并不涉及破解任何密码学。接着,攻击者会把这个高仿地址植入你的交易历史,方式要么是发送一笔极小额的 TRX,要么更巧妙一些——利用 TRC20 合约允许零金额转账这一点:一次 0 USDT 的 transferFrom 对攻击者来说成本很低,却会出现在你的历史记录里,看起来就像你曾经与这个地址互动过。

真正的回报在几周之后到来,当你从转账历史里复制「常用地址」,而不是从自己的记录里复制的时候。wallet 和区块浏览器都会把地址缩写成首尾字符,而这恰恰就是攻击者精心匹配的那部分字符,所以那个假地址乍一看完全正确。这种手法造成的损失并非纸上谈兵;在 2024 年一起被广泛报道的案例中,一名 Ethereum 用户把价值约 6800 万美元的 WBTC 发到了一个被投毒的地址。TRON 低廉的交易成本让这一步「埋雷」在这条链上变得更加便宜。

有效的防御方式并不炫酷。永远不要从交易历史中复制地址;应该从你自己的地址簿、数据库,或者收款方展示的接收界面中复制。核对地址时,比较的字符数要超过八位,或者更好的做法是完整比较一次整个字符串,之后就依赖一份 allowlist。对平台而言,规则应该是结构性的:出款目标地址来自你的数据库,一次性录入并核实,永远不会从 on-chain 历史中派生。测试转账的习惯在这里同样适用:一笔小额转账,在 Tronscan 上确认已被目标方收到,只需要几个 TRX,却胜过任何肉眼检查。

用代码校验一个 TRC20 地址

第一步的检查是用一个正则表达式对照 Base58 字母表:

const TRON_FORMAT = /^T[1-9A-HJ-NP-Za-km-z]{33}$/;

这个正则可以抓出长度错误和禁用字符,但抓不出字母表内部的打字错误。要做到这一点,需要校验校验和:

import bs58 from "bs58";
import { createHash } from "node:crypto";
const sha256 = (buf) => createHash("sha256").update(buf).digest();
function isValidTronAddress(address) {
if (!/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(address)) return false;
const decoded = Buffer.from(bs58.decode(address));
if (decoded.length !== 25 || decoded[0] !== 0x41) return false;
const payload = decoded.subarray(0, 21);
const checksum = decoded.subarray(21);
const expected = sha256(sha256(payload)).subarray(0, 4);
return expected.equals(checksum);
}

通过这项检查只能证明这个字符串是一个格式正确的 TRON 地址。它不能证明这个账户在链上真实存在,也不能证明有人持有对应的 key,更无法区分这是一个 wallet 还是一个合约。要回答合约相关的问题,需要查询 chain 或查看区块浏览器。

wallet 地址从何而来:TRON 上的 HD 派生

当一个 wallet 应用在你记下十二个单词后几秒钟内就给出一个 TRON 地址时,背后的机制是这样的。这十二个单词是一个 BIP-39 助记词,会展开成一个 seed。从这个 seed 出发,BIP-32 派生出一整棵 key 对组成的树,对同样的单词来说,派生出的树永远相同。接着 BIP-44 会通过一个注册过的 coin type,为每条 blockchain 分配自己的分支,TRON 对应的编号是 195,所以你第一个 TRON 地址的标准路径是 m/44'/195'/0'/0/0

这个 coin type 是值得记住的细节。Ethereum 对应 coin type 60,TRON 对应 195,这两个数字都出现在路径中经过强化(hardened)的层级上。同样的 seed 单词,却是完全不同的 key 树。这就是为什么同一份助记词在 EVM wallet 里会得到一个 0x 地址,而在 TRON wallet 里会得到一个毫不相关的 T 地址,也是为什么这两个 wallet 都看不到对方的资金。如果你在一个只支持 Ethereum 的 wallet 里恢复某份助记词,发现 TRON 余额似乎不见了,其实什么都没丢失;只是那个 wallet 从来没有派生过 195 这个分支。在支持 TRON 的 wallet 里恢复同样的单词,地址和余额都会重新出现。

路径末尾的索引会依次递增:.../0/1 是你用同样单词得到的第二个 TRON 地址,.../0/2 是第三个。个人 wallet 很少会用到超过几个的数量。平台的情况正好相反,对平台来说,更安全的做法是为每个客户独立生成 key,再通过 API 分别注册,因为这样一来,系统里就不会有单独一份助记词能控制所有的存款地址。

如何获取一个 TRC20 wallet 地址

有两条路径可选,具体选哪一条取决于你是想为自己获取一个地址,还是要为一个应用批量获取许多地址。

wallet 路径(单个地址,个人使用)

安装一个支持 TRON 的 wallet:TronLink 是这个网络标准的浏览器 wallet,Trust Wallet 覆盖移动端,Ledger 覆盖硬件设备。创建一个账户,离线记下 recovery phrase,然后打开接收界面。那里显示出来的 T... 地址,就是你用来接收 USDT 及其他任何 TRON token 的 TRC20 wallet 地址。创建它不需要任何费用,也不需要任何注册步骤;wallet 一旦生成地址,它就立刻可以使用。

API 路径(许多地址,面向应用)

一个需要按客户为存款入账的平台,需要为每个客户准备一个地址,而在 wallet 界面里点上一千次显然不现实。使用 Chaingateway 的 REST API,你可以在自己的环境中生成 key,再把它们注册到 TRON 网络。认证方式是 Bearer token:

Terminal window
curl -X POST https://app.chaingateway.io/api/v2/tron/addresses/import \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"address": "TYourGeneratedTronAddress",
"privatekey": "<generated secp256k1 private key, hex>",
"password": "<encryption password for this key>"
}'

加上请求头 X-Network: testnet,同一个调用就会指向 TRON 的 testnet,这样就可以先用免费的测试 TRX 把整个流程走一遍。之后发送 token 会走 POST /api/v2/tron/transactions/trc20 这个接口。quickstart 会带你完整走一遍这两个调用;无需 KYC 入驻的 7 天试用 足够覆盖测试阶段,各个方案则在价格页面上。

接收 TRC20 存款:webhook 流程

一旦每个客户都有了地址,剩下的问题就是如何在不轮询 Tronscan 的情况下察觉到存款。这个模式是:

  1. 为每个客户分配一个专属的 TRON 地址,并在数据库中保存这个对应关系。
  2. 注册一个 webhook。当一笔 TRC20 转账到达该地址时,你的 endpoint 会被调用,并带上交易数据。
  3. 校验 webhook 的 HMAC 签名,然后为客户入账。

投递是经过签名的,而你的 endpoint 错过的一次投递也不会丢失:GET /api/v2/tron/webhooks/notifications/failed 会列出所有从未成功送达的通知,POST /api/v2/tron/webhooks/notifications/{id}/retry 则可以重新发送。发生故障之后,GET /api/v2/tron/webhooks/notifications 会列出过去的通知供你对账。详细内容和签名校验代码都在 webhooks 指南中。

有一点是 TRON 特有的:接收是免费的,但把存款继续转出去需要消耗 energy 和 bandwidth。自从提案 #104 把 energy 价格从 210 Sun 降到 100 Sun 以来,如果不使用质押的 energy 支付,一笔 USDT 转账发到一个活跃地址大约要花 6.4 TRX,发到一个空地址大约要花 13.4 TRX。可以用 TRON 手续费计算器来规划归集成本;经常发送资金的人可以通过质押 TRX 换取 energy 来降低持续的消耗,这一点 API 通过 POST /api/v2/tron/freeze 来支持。

常见问题

是的。TRC20 地址只是在 TRC20 token 语境下使用的 TRON 账户地址。同一个 T... 地址可以持有 TRX、TRC10 token 和 TRC20 token。交易所提现页面上的「TRC20」标签指的是 token 标准和网络,不是一种特殊的地址类型。

34 个字符,以 T 开头。在 Base58Check 编码之下是 25 字节:前缀字节 0x41、20 字节的账户标识符,以及 4 字节的校验和。

TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t。这是 token 代码所在的位置,不是用来接收转账的地址。可以用它在 Tronscan 上验证 token,或手动把 USDT 添加到 wallet 中;直接发送到合约本身的转账是无法找回的。

那是同一个地址的十六进制写法。TRON node 的原始响应和交易 payload 使用带 41 前缀的 hex;wallet 和区块浏览器则使用以 T 开头的 Base58 形式。在代码中比较地址之前,请先把两者转换成同一种形式。

不可以,而且格式本身在这里起到保护作用:0x 地址无法通过 TRON 的验证,T... 地址也无法通过 Ethereum 的验证。真正的风险在于,在交易所的提现菜单里选错了网络,把你在另一条 chain 上持有的地址粘贴了进去。请始终让网络标签与地址格式相匹配。

不会。TRON 的 testnet(Shasta、Nile)使用相同的 0x41 前缀,所以 testnet 地址同样以 T 开头,并通过相同的验证。请在配置中把 testnet 和 mainnet 的 key 严格分开;地址格式本身不会在你混用时提醒你。

接收是免费的;发送需要消耗 energy 和 bandwidth。在没有质押 energy 的情况下,发送 USDT 到一个活跃的接收地址大约花费 6.4 TRX,发送到一个空地址大约花费 13.4 TRX,这是按照提案 #104 设定的 100 Sun energy 价格计算的。TRON 手续费计算器可以为你计算出当前的具体数字,而质押 TRX 换取 energy 可以降低你实际支付的费用。

交易大约 3 秒钟就能进入一个 block,并在 19 个 block 之后,大约 57 秒后被视为最终确认,前提是三分之二的 Super Representatives 已经在其之上构建了区块。大多数交易所会在这个时间窗口之后为 TRC20 存款入账,所以发送和入账之间相隔大约一分钟属于正常现象,并不是交易卡住了。

准备好自己动手构建了吗? 获取你的 API key — 7 天试用,无需信用卡 — 或查看 Tron API 获取完整的 endpoint 参考。

C
Chaingateway Team
区块链专家

Chaingateway 团队致力于为全球开发者简化区块链集成。