This specification introduces new URI schemes compatible with the Jamtis addressing scheme [1].
The URI encoding follows RFC 3986 [2] and has the following format:
SCHEME ":" PATH
The URIs are typically used to transfer information between devices. The PATH
segment of the URI only uses characters available in the alphanumeric encoding of QR codes [3] for maximum space efficiency. The URI should be converted to uppercase format when encoded in QR codes.
The payment URI is used to request a payment to be made. The URI scheme is "xmrpay" to disambiguate it from the legacy "monero" scheme [4].
SCHEME = "xmrpay"
The URI path encodes one or more invoices separated by the +
character.
PATH = INVOICE [ "+" INVOICE ...]
The invoice is encoded as a base32 string using the Jamtis base32 alphabet [5]. Each invoice starts with the prefix xmri
.
INVOICE = "xmri" <amount> <address> <version> <tagged-fields> <checksum>
The maximum permitted length of an invoice is 994 characters, which corresponds to the maximum length when the checksum algorithm can detect 5 errors.
The amount
is encoded in human-readable form as a decimal integer value (using digits 0-9
) with an optional unit suffix. The payment amount is calculated by applying a multiplier to the integral part depending on the suffix.
suffix | multiplier | unit |
---|---|---|
none | 1 | monero |
m |
10-3 | millinero |
u |
10-6 | micronero |
n |
10-9 | nanonero |
p |
10-12 | piconero |
The amount
should be encoded with the shortest possible representation (i.e. using the suffix with the largest multiplier or no suffix). For example, 0.0123 XMR is encoded as 12300u
.
The amount
may be set to an empty string to indicate an unspecified payment amount.
The address is a base32-encoded Jamtis address starting with the prefix xmra
[1].
Version is an integer encoded as a posvarint-16 (see Appendix A). The current version value is 1
. The software decoding the invoice shall abort if the version value is higher than the expected value.
The invoice can contain zero or more tagged fields with the following format:
TAGGED-FIELD = <tag> <length> <value>
tag
is a 1-character tag that encodes the field typelength
is the size of the data field (in multiples of 5 bits) encoded as a posvarint-16 (see Appendix A)value
is the content of the field (always multiple of 5 bits)
The following tagged fields are supported:
tag | length | field | value |
---|---|---|---|
n |
variable | recipient's name | text (see Appendix B) |
d |
variable | payment description | text (see Appendix B) |
e |
7 | payment expiration time | integer (35-bit, little endian) |
m |
variable | domain name | text (see Appendix B) |
k |
51 | public key | binary (255-bit, see Appendix C) |
s |
76 | signature | binary (380-bit, see Appendix C) |
The length must be included even for the constant-length fields for compatibility reasons.
The tagged fields must be sorted in the order they are listed in the table above. Duplicates are not allowed.
This is an arbitrary value that can be displayed to the user by the wallet software as the recipient's name. The recipient's name can also be loaded using the DNS-based lookup (§ 1.8).
This is an arbitrary value that can be displayed to the user by the wallet software as the payment description.
This is the date and time when the invoice will expire. The wallet software should warn the user that the invoice has expired. The value is encoded as the number of seconds since the Unix epoch, as a 35-bit unsigned integer. The 5-bit segments are ordered from the least significant bit (little-endian). The timestamp can encode dates until the year 3058.
This field specifies the domain name that has issued the invoice. It is used for the DNS-based lookup (§ 1.8).
This field encodes an ed25519 public key used to verify the invoice signature. For efficiency reasons, the public key is encoded only as the y
coodinate. The x
coodinate is implicitly chosen to be the even-parity value. The key generation procedure is specified in Appendix C. The public key can also be loaded using the DNS-based lookup (§ 1.8).
The optional signature signs all preceding data (the signature field is always the last tagged field of the invoice). The signature format is a short Schnorr signature [7] described in Appendix C, encoded in 380 bits (76 base32 characters).
The signed data are constructed by converting all base32 characters of the invoice preceding the signature field to binary and padding with 0-bits to the next byte-boundary.
The signature is only validated if a public key is available, either loaded using the DNS-based lookup (§1.8) or encoded in the field k
. If the signature is invalid or can't be validated, a warning should be displayed.
Accidental corruption of the invoice string is protected by an 8-character checksum that is calculated with the same algorithm as the Jamtis address checksum [6].
If the invoice contains the m
field (domain name) and the s
field (signature), the software reading the invoice should perform the following DNS lookup procedure.
- The TXT records associated with the domain name are fetched.
- The DNSSEC signature is verified. If DNSSEC is not configured or the chain of trust is invalid, the lookup procedure is aborted.
- The TXT records are searched for records matching the "xmrpay-info" scheme described in §1.8.2. There should be exactly one matching record, otherwise the procedure is aborted.
- The TXT record is parsed and the tagged fields in the record are inserted into the tagged field collection of the invoice. The fields from the TXT record take priority over the records encoded in the invoice.
The TXT record has the following URI format:
"xmrpay-info:" <version> [tagged-field-n] <tagged-field-k> <checksum>
The version
field follows the rules from §1.5. The version field can be optionally followed by the tagged field n
(recipient's name, §1.6.1) and the next item must be the tagged field k
(public key, §1.6.5). The checksum is calculated as in §1.7 and covers all characters after the colon.
The fields loaded using the DNS lookup override the n
and k
tagged fields present in the invoice. If the DNS lookup fails, the fields present in the invoice are used as a backup.
Payment of 239.39014 XMR with the description "donation" to an example Jamtis address:
xmrpay:xmri239390140uxmra1mj0b1977bw3ympyh2yxd7hjymrw8crc9kin0dkm8d3wdu8jdhf3fkd
pmgxfkbywbb9mdwkhkya4jtfn0d5h7s49bfyji1936w19tyf3906ypj09n64runqjrxwp6k2s3phxwm6
wrb5c0b6c1ntrg2muge0cwdgnnr7u7bgknya9arksrj0re7whkckh51ikxddtdx0natix0ntxau2sp8
This URI fits onto a 49x49 QR code (1320 bits).
The legacy URI equivalent would be:
monero:xmra1mj0b1977bw3ympyh2yxd7hjymrw8crc9kin0dkm8d3wdu8jdhf3fkdpmgxfkbywbb9md
wkhkya4jtfn0d5h7s49bfyji1936w19tyf3906ypj09n64runqjrxwp6k2s3phxwm6wrb5c0b6c1ntrg
2muge0cwdgnnr7u7bgknya9arksrj0re7whkckh51ik?tx_amount=239.39014&tx_description=d
onation
The legacy URI needs a 57x57 QR code (1976 bits).
The wallet keys URI is used to export and import wallet private keys. The URI scheme is "xmrwallet".
SCHEME = "xmrwallet"
The URI path is encoded as a base32 string with the prefix xmrw
.
PATH = "xmrw" <version> <tier-tag> <data> <checksum>
The version is encoded as a decimal number using digits 0-9
. The current version value is 1
.
Wallet tier | tag | data | total length |
---|---|---|---|
AddrGen | g |
sga, Dua, Dfr, Ks |
219 |
FindReceived | f |
birthday, dfr |
68 |
ViewReceived | r |
birthday, sga, dua, dfr, Ks |
221 |
ViewAll | a |
birthday, kvb, Ks |
119 |
The private and public keys are serialized according to the Jamtis notation [8]. The wallet birthday is serialized as a 10-bit number in the same format that is used by the Polyseed mnemonic phrase [9].
The "Master" wallet tier is not supported by the wallet keys URI. The mnemonic seed should be used instead.
The checksum is calculated over the whole PATH
segment and it uses the same algorithm as Jamtis [6].
TODO
- https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024
- https://www.ietf.org/rfc/rfc3986.txt
- https://en.wikipedia.org/wiki/QR_code#Encoding
- https://github.com/monero-project/monero/wiki/URI-Formatting#tx-scheme
- https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024#35-base32-encoding
- https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024#63-checksum
- https://eprint.iacr.org/2019/1105.pdf
- https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024#3-notation
- https://github.com/tevador/polyseed#wallet-birthday
Posvarint-16 is a variable-length encoding of a positive (non-zero) integral value using base32. The integer is encoded as a sequence of base32 characters (5 bits each), where the least significant 4 bits of each character is a little-endian base-16 digit of the integral, and the most significant bit indicates the next base32 character is a continuation of the current integral. The values of digits are offset by 1, i.e. digits {0}, {1}, ... {15}
have values {1}, {2}, ... {16}
.
values | number of digits |
---|---|
1-16 | 1 |
17-272 | 2 |
273-4368 | 3 |
Examples:
number | digits | base32 | decoding |
---|---|---|---|
7 |
(6) |
e |
=7*1 |
17 |
(16,0) |
kx |
=1*1+1*16 |
32 |
(31,0) |
9x |
=16*1+1*16 |
33 |
(16,1) |
km |
=1*1+2*16 |
200 |
(23,11) |
yf |
=8*1+12*16 |
Text can be represented in one of two encoding modes: Unicode or Alphanumeric. The encoder should select the encoding that produces fewer number of characters for a given string.
This mode can encode any Unicode string. The text is converted to a UTF8 byte sequence and then encoded using base32. For alphanumeric strings, this will typically be less efficient than the alphanumeric encoding unless the text needs many escape sequences.
The alphanumeric mode starts with the character t
, followed by a sequence of base32 characters. The character t
has a binary value of 10100
and no valid UTF8 string can start with this bit sequence.
In the alphanumeric mode, all base32 characters except of x
represent themselves. The character x
starts an escape sequence. The following escape sequences are supported in the alphanumeric mode:
escape | value |
---|---|
xx |
(space) |
xk |
x |
x0 |
o |
x1 |
l |
xw |
v |
x2 |
z |
xd |
. |
xh |
- |
xe |
! |
xq |
? |
xm |
, |
xu |
(the next character is uppercase) |
xc |
.co |
xg |
.org |
xn |
.net |
Examples:
text | encoded |
---|---|
example.com |
texkampx1excm |
Happy birthday! |
txuhappyxxbirthdayxe |
order no. 123456789 |
tx0rderxxnx0xd123456789 |
James Smith |
txujamesxxxusmith |
This signature scheme uses the ed25519 elliptic curve and consist of 3 functions: KeyGen
, Sign
and Verify
. The value ℓ = 2252 + 27742317777372353535851937790883648493
is the order of the prime subgroup of the elliptic curve.
The fuction Hx()
refers to the Blake2b hash function with an output length of x
bytes. If two parameters are provided, hashing is done in the keyed mode and the first parameter is the key. Concatenation is denoted by ||
.
- Generate
s = Random32()
- Calculate
(k,b) = H64(s, "eddsa_key_expansion")
- Calculate
K = k G
- If the
x
coordinate ofK
is odd, negate bothK
andk
. - Return the private parameters
(k,b)
and the public keyK
.
The public key is encoded as the 255-bit y
coordinate, in little endian byte order.
The Sign
function takes as input the private parameters (k,b)
and data
to be signed.
- Calculate
K = k G
- Set
i = 0
- Calculate
r = H64(b, "eddsa_nonce" || i || data) mod ℓ
- Calculate
R = r G
- Calculate
e = H16("eddsa_challenge" || R || K || data)
- Calculate
s = (r - e*k) mod ℓ
- If
s >= 2252
, incrementi
and go back to step 3. - Return the signature
(s,e)
The size of s
is 252 bits and the size of e
is 128 bits. The total size of the signature is 380 bits. s
is serialized first in little-endian byte order (32 bytes). Then e
is appended (16 bytes). Finally, the least significant 4 bits of e[15]
are copied to the most significant 4 bits of s[31]
(these bits are always zero because s < 2252
). When the signature is serialized in base32, the redundant least significant 4 bits of e[15]
are omitted, so the signature fits exactly into 76 characters.
The Verify
function accepts a signature (s,e)
, a public key K
and data
that was signed.
- Calculate
R = s G + e K
- Calculate
e' = H16("eddsa_challenge" || R || K || data)
- Check that
e ?= e'
Nice work, thanks.
Some comments:
xmrreq
(or leave it as-is).A
). Alternatively, put the version directly in at the beginning of the top-level path instead of per-invoice (not sure you'd ever want a URI containing invoices of different types).k
andn
- is there an implied/undocumented rule that there be only one of each of these fields?xmrw
in wallet URIs? It looks a bit redundant here (it seems like theversion
would be sufficient to handle changing formats).