tip: 712
title: TRON typed structured data hashing and signing
author: yanghang8612@163.com
discussions to: https://github.com/tronprotocol/TIPs/issues/443
status: Final
type: Standards Track
category: Interface
created: 2022-07-25
This TIP introduces a standard for hashing and signing of typed structured data as opposed to just bytestrings into TRON protocol to solve the problem that hashing structured data is non-trivial and errors result in loss of the security properties of the system, etc.
The encoding function, hashing algorithm and signing algorithm is identical to EIP-712. There are some differences in details. It includes a
address
: need to remove TRON unique prefixe(0x41
) and encoded asuint160
trcToken
: should be treated as atomic type and encoded asuint256
chainId
: useblock.chainid & 0xffffffff
instead
This TIP aims to improve the usability of off-chain message signing for use on-chain. We are seeing growing adoption of off-chain message signing as it saves transaction fees and reduces the number of transactions on the blockchain. Currently signed messages are an opaque hex string displayed to the user with little context about the items that make up the message.
A signature scheme consists of hashing algorithm and a signing algorithm. A good hashing algorithm should satisfy security properties such as determinism, second pre-image resistance and collision resistance. Otherwise, it may cause security problems or replay problems, etc. This standard aims to solve these problems.
For the structured data 𝕊, they are encoded to bytestrings suitable for hashing and signing as follows:
encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
wheredomainSeparator
andhashStruct(message)
are defined below.
The encoding is compliant with TIP-191 and it`s totaly compatible with EIP-712
. The 'version byte' is fixed to 0x01
, the version specific data
is the 32-byte domain separator domainSeparator
and the data to sign
is the 32-byte hashStruct(message)
.
Definitions for all types are identical to EIP-712
.
The hashStruct
function is defined as
hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))
wheretypeHash = keccak256(encodeType(typeOf(s)))
It's totaly compatible with EIP-712
.
The type of a struct is encoded as name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"
where each member is written as type ‖ " " ‖ name
.
It's totaly compatible with EIP-712
.
For trcToken
, it should be treated as atomic type.
struct AssetTransfer {
address from;
address to;
trcToken id;
uint256 amount;
uint8 v;
bytes32 r;
bytes32 s;
}
For example, the above AssetTransfer
struct is encoded as AssetTransfer(address from,address to,trcToken id,uint256 amount,uint8 v,bytes32 r,bytes32 s)
.
The encoding of a struct instance is enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)
, i.e. the concatenation of the encoded member values in the order that they appear in the type. Each encoded member value is exactly 32-byte long.
It's totaly compatible with EIP-712
.
The only difference between TRON address and Ethereum address is that TRON address starts with a byte prefix 0x41
and uses base58
encoding, so prefix needs to be removed when the address type is processed.
domainSeparator = hashStruct(tip712Domain)
where the type of tip712Domain
is identical to EIP712Domain
struct defined in EIP-712
. It also contains one or more of the below fields.
string name
the user readable name of signing domain.string version
the current major version of the signing domain. Signatures from different versions are not compatible.uint256 chainId
the chain id. The user-agent should refuse signing if it does not match the currently active chain.address verifyingContract
the address of the contract that will verify the signature. The user-agent may do contract specific phishing prevention.bytes32 salt
an disambiguating salt for the protocol. This can be used as a domain separator of last resort.
The only difference is that we define the chainId
data used in the domainSeparator
to be equal to block.chainid & 0xffffffff
. In other words, the chainId
data used in the domainSeparator
only take the higher four bytes of block.chainId
.
The formula for calculating the chainid
in domainSeparator
is as follows:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ChainIdExample {
// after solidity_v0.8.0
function getChainId() public view returns(bytes32,uint256) {
uint256 chainId = block.chainid & 0xffffffff;
return (bytes32(chainId), chainId);
}
// before solidity_v0.8.0
function getChainIdAssembly() public view returns(bytes32,uint256) {
uint256 chainId;
assembly {
chainId := and(chainid(), 0xffffffff)
}
return (bytes32(chainId), chainId);
}
}
In HEX:
0x000000000000000000000000000000000000000000000000000000002b6653dc
In Decimal:
728126428
In HEX:
0x00000000000000000000000000000000000000000000000000000000cd8690dc
In Decimal:
3448148188
The FullNode gRPC calls and SomeStruct.typeHash
parameter are currently undefined. Defining them should not affect the behaviour of existing DApps.
The Solidity expression keccak256(someInstance)
for an instance someInstance
of a struct type SomeStruct
is valid syntax. It currently evaluates to the keccak256
hash of the memory address of the instance. This behaviour should be considered dangerous. In some scenarios it will appear to work correctly but in others it will fail determinism and/or injectiveness. DApps that depend on the current behaviour should be considered dangerously broken.