← All articles

Contract Accounts vs. Externally Owned Accounts

Key takeaways

  • Every TRON account is one of two kinds: an externally owned account (EOA), controlled by whoever holds its private key, or a contract account, controlled through its deployed code.
  • A contract address is computed from the deploy transaction's hash and the deployer's address — there is no public key behind it, and no private key either.
  • Both kinds carry the same 0x41 / "T" prefix, so the address string can't tell them apart. The account's type field, wallet/getcontract, and the ISCONTRACT opcode can.
  • An account nobody can sign for can't control another account — so a contract can be ruled out as a wallet's controller, however much it dominates that wallet's history.

TRON’s ledger holds two kinds of account. An externally owned account — an EOA — is “an account controlled by anyone with the corresponding private key.” A contract account is a smart contract deployed to the network and controlled through its code. Every address in a trace resolves to one or the other.

On the surface they’re indistinguishable. Both are 21-byte addresses behind the same 0x41 prefix, both render as a Base58 string starting with “T”, both hold balances and accumulate transaction history. The chain knows exactly which is which; the string tells you nothing.

The difference settles an attribution question before the trace even starts. TRON’s documentation states it flatly: “No one owns the private key of a contract account.” No key means no signature, and a party that can’t sign isn’t steering another account — so a quick type check is the cheapest disqualification test in the toolkit.

A key on one side, code on the other

An EOA is operated by signature. Per TRON’s account documentation, “a private key signature is required for operating the account” — the holder of the key can move TRX and TRC-10 balances, deploy smart contracts, and trigger contracts released by itself or others. The key pair is ECDSA over secp256k1, “exactly the same as that of Ethereum,” and the full pipeline from that key to the “T” address is covered in How a TRON Address Is Built. Which keys get to sign, and with what weight, is governed by the permission structure every account carries — that layer lives in The TRON Account Model.

A contract account exists because someone deployed code. What the account does is fixed by its bytecode, and there is no key that operates it — the code answers calls, and nothing else happens. The split itself predates TRON: Ethereum draws the same line between an account “controlled by anyone with the private keys” and “a smart contract deployed to the network, controlled by code,” and TRON inherits the concept along with the virtual machine. The mechanics below, though, are TRON’s own.

How a contract account comes to exist

Deployment is the only birth path. A contract account is created by a CreateSmartContract transaction — contract type 30 in the protocol’s transaction-type enum — built through the Fullnode API’s wallet/deploycontract endpoint. The deploy carries the contract’s compiled bytecode as a parameter, and the network’s response hands back the new account’s address in a contract_address field: a hex string beginning with the familiar 41, like 416c5d359d1836085cdad65788e1ce53d3d7a13dd6.

What lands on-chain is a SmartContract record whose fields are the account: origin_address, contract_address, abi, bytecode, and code_hash — the hash of that bytecode. Note what signed the deploy: an EOA. A contract account is born from someone else’s signature, the record preserves who in origin_address, and from that point the code runs inside the TVM — the execution model is the subject of Smart Contracts and the TVM.

Where a contract address comes from

An EOA address is the hash of a public key; there is always a private key standing behind it. A contract address is not derived from a public key at all. For an ordinary top-level deploy, java-tron computes it as sha3omit12(txRawDataHash ‖ ownerAddress) — the deploy transaction’s raw-data hash concatenated with the deployer’s address, run through Keccak-256, trimmed to the last 20 bytes, and prefixed with 0x41. The Developer Hub describes the same recipe in prose: the address is “calculated based on the ID of the deployed contract transaction and the sender’s account address.”

Every input to that function is public. No key material enters it, so no private key exists that corresponds to the output — the address is an identifier for code, permanently unsignable.

EOA CONTRACT private key secp256k1 public key ECDSA Keccak-256 hash of public key TJmuRk…9Yt3 0x41-prefixed address CAN SIGN a private key stands behind it DERIVE HASH PREFIX 0x41 no private key exists deploy-tx hash ‖ deployer address every input is public — no key material sha3omit12(txHash ‖ deployerAddr) Keccak-256, last 20 bytes TJmuRk…9Yt3 0x41-prefixed address CANNOT SIGN no key stands behind it APPLY PREFIX 0x41 SAME 0x41 / T-ADDRESS string can't tell them apart
Two derivations, one address shape — only the EOA has a key behind it.

There’s a second formula for the internal case. When a contract deploys another contract through the TVM’s CREATE path, the child’s address is sha3omit12(rootTransactionId ‖ nonce) — the root transaction’s ID plus a nonce that is a per-root-transaction counter, incrementing with each internal action, rather than Ethereum’s per-account sender nonce. Two distinct formulas for two creation paths, and neither is byte-identical to Ethereum’s RLP-of-sender-and-nonce scheme — an Ethereum address prediction does not port to TRON.

Telling them apart on-chain

The string can’t answer, so you ask the chain, and it will answer three ways.

The plainest is the account’s type field. The protocol’s Account message carries an AccountType enum — Normal = 0, AssetIssue = 1, Contract = 2 — and a deployed contract reads type 2 the moment you pull its record.

A wallet/getcontract query digs deeper. The endpoint resolves a contract address to its SmartContract record: bytecode, ABI, name, code_hash, and origin_address, documented as the “Address of the contract creator.” An EOA has no such record on file, so the call comes back with nothing — and the silence is itself the answer.

The last discriminator lives inside the machine. Contracts need the same fact at execution time, so the TVM carries a dedicated opcode, ISCONTRACT (0xD4), to “test whether an address is a contract” — one of the TRON-specific opcodes covered with the rest of the TVM’s instruction set.

TJmuRk…9Yt3 which kind of account? TYPE FIELD Account.type = Contract (2) AccountType enum Normal 0 · AssetIssue 1 Contract 2 RPC QUERY wallet/getcontract → SmartContract record bytecode · ABI origin_address = deployer EOA → nothing to return TVM OPCODE ISCONTRACT opcode 0xD4 TVM instruction tests whether an address is a contract, at execution ANSWER: CONTRACT onChainContract an address that resolves to a contract is DISQUALIFIED AS CONTROLLER code, not a key-holder — it cannot sign, so it cannot steer
Three ways to ask the chain, and the attribution consequence once it answers.

The origin_address field deserves its own note. It is a durable on-chain pointer from the contract back to the EOA that deployed it — a permanent answer to “who put this code here.” That is the attribution-relevant fact a contract carries, and it’s distinct from control: no one controls the contract by key, but a specific wallet chose to sign its deployment, and the record never forgets which.

The attribution consequence

The logic runs in a straight line. Operating an account requires a private-key signature. No one owns the private key of a contract account. So a contract cannot sign, cannot initiate a transaction of its own, and cannot be the controlling party of another account. A contract only ever executes the calls it is handed — the intent belongs to whichever EOA sent them.

An address with no key behind it cannot sign, and an address that cannot sign cannot be anyone's controller.

This settles the attribution question directly. When a wallet’s most prominent counterparty resolves to a deployed contract, that contract can be ruled out as its controller: a contract is code, not a key-holder, so it cannot steer the wallet no matter how large it looms in the history. A token contract or router that dominates a wallet’s transaction volume is infrastructure the wallet touched — an instrument in its history rather than the hand on its keys. The productive move is the redirect: wallet/getcontract gives you the contract’s origin_address, and the deployer is an EOA that did sign something.

Run the type check early. A candidate address that turns out to be a contract closes one line of inquiry and opens a better one — off the instrument, onto the wallet that deployed it and the wallets that call it. On a chain where a wallet and a contract wear the same “T”, knowing which kind of account you’re looking at is the difference between chasing an actor and chasing a tool.

Sources