THIS IS A DRAFT
This document proposes a standard for QR code encoding that enables two-way communication between a Hot Wallet and a Cold Signer with access to private keys, for the purpose of securely signing and broadcasting transactions and data on current and future decentralized networks (including non-Ethereum networks). The goal is have a single, inter-operable standard, allowing users to use any combination of a Hot Wallet and a Cold Signer (defined below) without vendor locking.
The common ways to encode binary data in a QR code would include:
For data density and simplicity this standard will only use the native Binary QR encoding.
Note: Base64 US-ASCII representation with Alphanumeric QR encoding is impossible, as Alphanumeric QR code only permits 44 (5½ bits per character) out of the required 64 characters (6 bits per character).
Since this technology requires two separate devices/applications, to avoid confusion the following names will be used to differentiate the two:
For describing binary data this standard uses either a single byte index [n]
, an open left-inclusive range [n..]
, or a closed left-incluse right-exclusive range [n..m]
. [..n]
is a shorthand for [0..n]
. Examples:
[3]
is a single byte at index 3
.[0..5]
is 5 bytes at following indexes: 0
, 1
, 2
, 3
, and 4
.[..5]
is also 5 bytes at following indexes: 0
, 1
, 2
, 3
, and 4
.[7..7]
would be a zero-length range and contain no bytes.[10..]
would be all bytes starting from index 10
till the end.For byte values this standard uses either a single hexadecimal value AA
, or a range AA...BB
, which is left and right inclusive:
00
is a single US-ASCII nul
byte.61...7A
is a range including all lowercase US-ASCII letters a
to z
.Additionally we will define the following terms to mean:
Since this is a multi-step process, we will differentiate between the following types of QR codes:
Step | Name | Direction | Contains | QR Encoding |
---|---|---|---|---|
0⁽¹⁾ | Introduction | Cold ⇒ Hot | Network identification and Address | Binary (UTF-8) |
1 | Payload | Cold ⇐ Hot | Data to sign prefixed with metadata | Binary |
2 | Signature | Cold ⇒ Hot | Signature for Payload | Binary |
The goal of this step is for Cold Signer to inform the Hot Wallet about a single account it has access to. To make this useful outside of the scope of this specification, this standard proposes using URI format compatible with EIP-681 and EIP-831, with syntax:
introduction = scheme ":" details
scheme = STRING
details = STRING
details
format depends on the scheme
.scheme
MUST be valid US-ASCII, beginning with a letter and followed by any number of letters, numbers, the period .
character, the plus +
character, or the hyphen -
character.details
MUST be valid UTF-8, appropriate for a given network.scheme
and details
to the string."Scheme {scheme} is not supported by {wallet name}"
.details = address | address "@" chainid | address "@" chainid ":" name
scheme
MUST be a string ethereum
.address
MUST be a hexadecimal string representation of the address.address
MUST be prefixed with 0x
chainid
MUST be a decimal number.chainid
SHOULD map onto a proper value at https://chainid.network/.name
is an optional display name.A correct Introduction for address zero (0x0000000000000000000000000000000000000000
) on Ethereum is therefore a string:
ethereum:0x0000000000000000000000000000000000000000@1
details = address | address ":" genesishash | address ":" genesishash ":" name
scheme
MUST be a string substrate
.address
MUST be base58 representation of the address.genesishash
MUST be a hexadecimal representation of the genesis hash of a given substrate network.genesishash
MUST be prefixed with 0x
.name
MUST be valid UTF-8 and can include the character :
.A correct Introduction for address 5GKhfyctwmW5LQdGaHTyU9qq2yDtggdJo719bj5ZUxnVGtmX
on a Substrate-based network is therefore a string:
substrate:5GKhfyctwmW5LQdGaHTyU9qq2yDtggdJo719bj5ZUxnVGtmX
Payload is always read left-to-right, using prefixing to determine how it needs to be read. The first prefix is single byte at index 0
:
[0] |
[1..] |
---|---|
00 |
Multipart Payload |
01...44 |
Extension range for other networks |
45 |
Ethereum Payload |
46...52 |
Extension range for other networks |
53 |
Substrate Payload |
54...7A |
Extension range for other networks |
7B |
Legacy Ethereum Payload |
7C...7F |
Extension range for other networks |
80...FF |
Reserved |
QR codes can only represent 2953 bytes, which is a harsh constraint as some transactions, such as contract deployment, may not fit into a single code. Multipart Payload is a way to represent a single Payload as a series of QR codes. Each QR code in Multipart Payload, or a frame, looks as follows:
[0] |
[1..3] |
[3..5] |
[5..] |
---|---|---|---|
00 |
frame |
frame_count |
part_data |
frame
MUST the number of current frame, represented as big-endian 16-bit unsigned integer.frame_count
MUST the total number of frames, represented as big-endian 16-bit unsigned integer.part_data
MUST be stored by the Cold Signer, ordered by frame
number, until all frames are scanned.part_data
for frame 0
MUST NOT begin with byte 00
or byte 7B
.Once all frames are combined, the part_data
must be concatenated into a single binary blob, and then interpreted as a completely new albeit larger Payload, starting from the prefix table above.
Byte 45
is the US-ASCII byte representing the capital letter E
. Ethereum Payload follows the table:
Action | [0] |
[1] |
[2..22] |
[22..] |
---|---|---|---|---|
Sign a hash | 45 |
00 |
address |
hash |
Sign a transaction | 45 |
01 |
address |
rlp |
Sign a message | 45 |
02 |
address |
message |
address
MUST NOT have any prefixes.address
MUST be exactly 20 bytes long.address
MUST be represented as a binary byte string, NOT hexadecimal.rlp
MUST be the RLP encoded raw transaction with an empty signature being set in accordance with EIP-155: v = CHAIN_ID
, r = 0
, s = 0
.message
MUST be a binary or UTF-8 encoded message to sign WITHOUT any prefixes (EIP-191 or otherwise).hash
MUST be a valid keccak256
hash of either a transaction or a correctly prefixed message.message
as UTF-8 encoded human readable string by whatever heuristics it finds suitable and display it to the user, so that the user can verify that the message hasn't been altered by the Hot Wallet.TODO: Handle EIP-712 typed data.
Byte 53
is the US-ASCII byte representing the capital letter S
. Substrate Payload follows the table:
Action | [0] |
[1] |
[2] |
[1..1+L] |
[1+L..] |
---|---|---|---|---|---|
Sign a transaction | 53 |
crypto |
00 |
accountid |
payload |
Sign a transaction | 53 |
crypto |
01 |
accountid |
payload_hash |
Sign an immortal transaction | 53 |
crypto |
02 |
accountid |
immortal_payload |
Sign a message | 53 |
crypto |
03 |
accountid |
message |
crypto
MUST be a recognised cryptographic algorithm. It implies the value of the accountid
length, L
. This MUST be one byte whose value is one of:
0x00
: Ed25519 (L = 32
)0x01
: Schnorr/Ristretto x25519 (L = 32
)accountid
MUST be exactly L
bytes long.accountid
MUST be represented as a binary byte string, NOT hexadecimal.payload
MUST be the SCALE encoding of the tuple of transaction items (nonce, call, era_description, era_header)
.payload_hash
MUST be the Blake2s 32-byte hash of the SCALE encoding of the tuple of transaction items (nonce, call, era_description, era_header)
.immortal_payload
MUST be the SCALE encoding of the tuple of transaction items (nonce, call)
.00
for signing a standard transaction type if the length of the payload
is 256 bytes or fewer.00
even if the length of the payload is greater than 256 bytes since this allows the full payload to be provided and decoded for the user. If doing that is completely impractical (the message or the transaction is megabytes long and not suitable for Multipart Payload), type 01
may be used alternatively.message
as UTF-8 encoded human readable string by whatever heuristics it finds suitable and display it to the user for verification before signing.01
.message
, immortal_payload
, or payload
if payload
is of length 256 bytes or fewer. If payload
is longer than 256 bytes, then it SHOULD instead sign the Blake2s hash of payload
.Byte 7B
is the US-ASCII byte representing open curly brace {
, for that reason it's treated as a prefix for older, deprecated format. This Payload should be decoded in full as UTF-8 encoded JSON, following either of the two variants:
{
"action": "signTransaction",
"data": {
"account": ADDRESS,
"rlp": RLP
}
}
or
{
"action": "signData",
"data":{
"account": ADDRESS,
"data": MESSAGE
}
}
ADDRESS
MUST be a hexadecimal string representation of the address, exactly 40 characters long.ADDRESS
MUST NOT include the 0x
prefix.RLP
MUST be a hexadecimal string representation of the RLP encoded raw transaction with an empty signature being set in accordance with EIP-155: v = CHAIN_ID
, r = 0
, s = 0
.RLP
MUST NOT include the 0x
prefix.DATA
MUST be a hexadecimal string representation of a binary or UTF-8 encoded message to sign WITHOUT any prefixes (EIP-191 or otherwise).DATA
MUST NOT include the 0x
prefix.Signatures will vary on type of payload that is being signed.
Ethereum signature must follow one of the two following formats:
[0] |
[1..33] |
[33..65] |
[66] |
---|---|---|---|
01 |
r |
s |
v |
or
[0..64] |
[64..128] |
[128..130] |
---|---|---|
HEX_R |
HEX_S |
HEX_V |
130
.r
MUST be binary r
value of the Secp256k1 signature for the signed Payload.s
MUST be binary s
value of the Secp256k1 signature for the signed Payload.v
MUST be binary v
value of the Secp256k1 signature for the signed Payload.HEX_R
MUST be a hexadecimal representation of r
value of the Secp256k1 signature for the signed Payload.HEX_S
MUST be a hexadecimal representation of s
value of the Secp256k1 signature for the signed Payload.HEX_V
MUST be a hexadecimal representation of b
value of the Secp256k1 signature for the signed Payload.HEX_R
, HEX_S
, and HEX_V
MUST NOT be prefixed with 0x
.v
and HEX_V
MUST NOT be combined with CHAIN_ID
.CHAIN_ID
into the v
value when constructing final transaction RLP.Pseudocode for folding in CHAIN_ID
into v
:
if chainId > 0 {
v += (chainId * 2 + 8) & 0xFF;
}
TODO
Copyright and related rights waived via CC0.