Skip to content

Instantly share code, notes, and snippets.

@tevador
Last active January 1, 2023 22:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tevador/500d5d32d5ecc73b56997e12a9d2b20e to your computer and use it in GitHub Desktop.
Save tevador/500d5d32d5ecc73b56997e12a9d2b20e to your computer and use it in GitHub Desktop.

Jamtis URI Schemes

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.

1. Payment request URI

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 ...]

1.2 Invoice encoding

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.

1.3 Human-readable amount

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.

1.4 Address

The address is a base32-encoded Jamtis address starting with the prefix xmra [1].

1.5 Version

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.

1.6 Tagged fields

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 type
  • length 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.

1.6.1 Recipient's name

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).

1.6.2 Payment description

This is an arbitrary value that can be displayed to the user by the wallet software as the payment description.

1.6.3 Payment expiration

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.

1.6.4 Domain name

This field specifies the domain name that has issued the invoice. It is used for the DNS-based lookup (§ 1.8).

1.6.5 Public key

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).

1.6.6 Signature

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.

1.7 Checksum

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].

1.8 DNS-based lookup

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.

1.8.1 DNS Lookup

  1. The TXT records associated with the domain name are fetched.
  2. The DNSSEC signature is verified. If DNSSEC is not configured or the chain of trust is invalid, the lookup procedure is aborted.
  3. 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.
  4. 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.

1.8.2 TXT record format

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.

1.9 Examples

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).

2. Wallet keys URI

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>

2.1 Version

The version is encoded as a decimal number using digits 0-9. The current version value is 1.

2.2 Wallet tier and data

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.

2.3 Checksum

The checksum is calculated over the whole PATH segment and it uses the same algorithm as Jamtis [6].

2.4 Examples

TODO

References

  1. https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024
  2. https://www.ietf.org/rfc/rfc3986.txt
  3. https://en.wikipedia.org/wiki/QR_code#Encoding
  4. https://github.com/monero-project/monero/wiki/URI-Formatting#tx-scheme
  5. https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024#35-base32-encoding
  6. https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024#63-checksum
  7. https://eprint.iacr.org/2019/1105.pdf
  8. https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024#3-notation
  9. https://github.com/tevador/polyseed#wallet-birthday

Appendix A: posvarint-16 encoding

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

Appendix B: Text encoding

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.

B.1 Unicode encoding

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.

B.2 Alphanumeric encoding

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

Appendix C: Short Schnorr signature

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 ||.

C.1 KeyGen

  1. Generate s = Random32()
  2. Calculate (k,b) = H64(s, "eddsa_key_expansion")
  3. Calculate K = k G
  4. If the x coordinate of K is odd, negate both K and k.
  5. Return the private parameters (k,b) and the public key K.

The public key is encoded as the 255-bit y coordinate, in little endian byte order.

C.2 Sign

The Sign function takes as input the private parameters (k,b) and data to be signed.

  1. Calculate K = k G
  2. Set i = 0
  3. Calculate r = H64(b, "eddsa_nonce" || i || data) mod ℓ
  4. Calculate R = r G
  5. Calculate e = H16("eddsa_challenge" || R || K || data)
  6. Calculate s = (r - e*k) mod ℓ
  7. If s >= 2252, increment i and go back to step 3.
  8. Return the signature (s,e)

C.2.1 Signature serialization

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.

C.3 Verify

The Verify function accepts a signature (s,e), a public key K and data that was signed.

  1. Calculate R = s G + e K
  2. Calculate e' = H16("eddsa_challenge" || R || K || data)
  3. Check that e ?= e'
@UkoeHB
Copy link

UkoeHB commented Dec 31, 2022

Nice work, thanks.

Some comments:

  • Consider renaming 'payment URI' to 'payment request URI', so it's clearly disambiguated from the idea of a 'received payment'. You could also make the scheme xmrreq (or leave it as-is).
  • Does putting the version number after the address mean the address format is 'baked in', and the version only controls tagged fields? Idk if this is good or bad, but it may be worth noting. One disadvantage of versioning after the address is it's impossible to visually identify the version. If you are concerned about the version number interfering with the amount, you could use letters instead (starting with version 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).
  • The tagged fields looks like Type-Length-Value format. A) TLV is a fairly standard encoding scheme so it may be good to follow that terminology (not that important though). B) I recommend enforcing sorted TLV - i.e. sorting by tag then length then value (improves implementation uniformity a little bit).
  • Maybe make a note that with 35 bits you get a unix timestamp into year 3058 (more than enough for us).
  • Instead of bespoke key gen for signatures, you could record the parity bit as part of the tag (i.e. use two tags or unused bits of the tag). Doing that might be even more work though - probably fine to leave it as-is.
  • The DNS lookup overrides k and n - is there an implied/undocumented rule that there be only one of each of these fields?
  • What is the reason for including prefix xmrw in wallet URIs? It looks a bit redundant here (it seems like the version would be sufficient to handle changing formats).
  • I recommend renaming your wallet URI to account URI, to disambiguate from 'wallet file data' which can include a lot more than just keys (at first glance I thought your goal was to have a URI for transmitting all wallet data, which on further thought doesn't make sense given the 995 char limit).
  • You may want to add a comment explaining why master accounts don't get a URI spec (i.e. seeds already have their own specs).
  • Appendix A confused me quite a bit. "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 first 4 bits of each character is a little-endian base-16 digit of the integral, and setting the most significant 5th bit indicates the next base32 character is a continuation of the current integral]."

@UkoeHB
Copy link

UkoeHB commented Dec 31, 2022

Comments on Appendix C:

  • Notation issue: ed25519 encodes with the y coordinate plus sign bit of the x coordinate (section 5). It is X25519 where points are encoded with the x coordinate (iirc).
  • I recommend putting the domain separator at the beginning of hash contents. That way you can trivially use the transcript utility I built. SpKDFTranscript will build the hash strings you specify, then you can easily call into the jamtis hash functions. If your goal is using blake2b's keyed hash mode, then you won't end up with those exact hash strings, so the spec may be misleading (maybe you are getting at that with the comma , vs pipes ||? I think you'd only want a keyed hash for deriving r though).
  • I remarked on this notation before - r for nonce and e for challenge and s for response is headache-inducing. Why not a or n for nonce, c for challenge, and r for response?
  • Why are the least significant 4 bits of e[15] displaced instead of the most significant 4 bits? That would seem to be more intuitive and easier to serialize.

@tevador
Copy link
Author

tevador commented Dec 31, 2022

Thanks for the review. I did some modifications based on your comments.

Consider renaming 'payment URI' to 'payment request URI', so it's clearly disambiguated from the idea of a 'received payment'. You could also make the scheme xmrreq (or leave it as-is).

I changed the name to "Payment request URI" for clarity. I think the scheme is fine.

Does putting the version number after the address mean the address format is 'baked in', and the version only controls tagged fields? Idk if this is good or bad, but it may be worth noting. One disadvantage of versioning after the address is it's impossible to visually identify the version. If you are concerned about the version number interfering with the amount, you could use letters instead (starting with version 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).

Addresses have their own version. I wanted to aggregate the human-readable parts at the beginning of the invoice. This is the human-readable prefix from the example:

xmri239390140uxmra1m
  • xmri identifies an XMR invoice
  • 239390140u is the amount
  • xmra identifies an XMR address
  • 1 is the version of the address
  • m means it's a mainnet address

Having two different versions in the header might be confusing. Placing the invoice version after the address only prevents changes to the amount field, which are unlikely to be needed.

The tagged fields looks like Type-Length-Value format. A) TLV is a fairly standard encoding scheme so it may be good to follow that terminology (not that important though). B) I recommend enforcing sorted TLV - i.e. sorting by tag then length then value (improves implementation uniformity a little bit).

I renamed it to tag, length, value and specified the sorting requirements and the fact that duplicates are not permitted.

Maybe make a note that with 35 bits you get a unix timestamp into year 3058 (more than enough for us).

Added.

What is the reason for including prefix xmrw in wallet URIs? It looks a bit redundant here (it seems like the version would be sufficient to handle changing formats).

It is intended as a human-readable prefix in case the string is used without the scheme, which is possible (same as the invoice) and also for uniformity:

  • xmra is the address prefix
  • xmri is the invoice prefix
  • xmrw is the wallet prefix

I recommend renaming your wallet URI to account URI, to disambiguate from 'wallet file data' which can include a lot more than just keys (at first glance I thought your goal was to have a URI for transmitting all wallet data, which on further thought doesn't make sense given the 995 char limit).

"Account" might be confused with accounts that are used to aggregate addresses. I think "wallet" is the correct term here.

You may want to add a comment explaining why master accounts don't get a URI spec (i.e. seeds already have their own specs).

Added.

Appendix A confused me quite a bit.

Rephrased.

Notation issue: ed25519 encodes with the y coordinate plus sign bit of the x coordinate (section 5). It is X25519 where points are encoded with the x coordinate (iirc).

Fixed.

I recommend putting the domain separator at the beginning of hash contents. That way you can trivially use the transcript utility I built. SpKDFTranscript will build the hash strings you specify, then you can easily call into the jamtis hash functions. If your goal is using blake2b's keyed hash mode, then you won't end up with those exact hash strings, so the spec may be misleading (maybe you are getting at that with the comma , vs pipes ||? I think you'd only want a keyed hash for deriving r though).

The domain separator is at the beginning. The first argument is the key (for keyed mode). I clarified the notation.

I remarked on this notation before - r for nonce and e for challenge and s for response is headache-inducing. Why not a or n for nonce, c for challenge, and r for response?

AFAIK this is the standard notation for Schnorr signatures. It is used, for example, in the paper I'm referencing.

Why are the least significant 4 bits of e[15] displaced instead of the most significant 4 bits? That would seem to be more intuitive and easier to serialize.

When encoding the 48-byte signature to base32, the last 3 bytes will look like this:

|      e[13]    |      e[14]    |      e[15]    |
|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
+---------+-----+---+---------+-+-------+-------+-+
|4 3 2 1 0|4 3 2 1 0|4 3 2 1 0|4 3 2 1 0|4 3 2 1 0|
|  b[72]  |  b[73]  |  b[74]  |  b[75]  |  b[76]  |

If we want to encode the signature in just 76 characters, b[76] must be omitted, so the least significant bits of e[15] must be displaced.

@UkoeHB
Copy link

UkoeHB commented Dec 31, 2022

"Account" might be confused with accounts that are used to aggregate addresses. I think "wallet" is the correct term here.

What about 'wallet keys URI' or 'wallet generation URI'?

Having two different versions in the header might be confusing. Placing the invoice version after the address only prevents changes to the amount field, which are unlikely to be needed.

I feel like knowing the invoice version would be a lot more useful than the address version when manually examining an invoice. Maybe people who will use these URIs can weigh in. Note that pulling out the invoice version would save 1 character per additional invoice.

When encoding the 48-byte signature to base32, the last 3 bytes will look like this:

This is how the encoding might look at the machine level, but in an abstract sense the most significant 4 bits of the last byte of a little-endian byte string are the 'last bits'. I imagine this will be confusing to a lot of other people too. Is there some performance or implementation reason for displacing the least significant vs most significant bits?

@UkoeHB
Copy link

UkoeHB commented Dec 31, 2022

AFAIK this is the standard notation for Schnorr signatures.

It just means doing a lot of mental work every time you (or at least I) look at the scheme, instead of only one time when reading the paper.

@tevador
Copy link
Author

tevador commented Dec 31, 2022

What about 'wallet keys URI'

That might work.

I feel like knowing the invoice version would be a lot more useful than the address version when manually examining an invoice.

Why? The invoice version only affects the tagged fields and the checksum, which are not meant to be human-readable.

Note that pulling out the invoice version would save 1 character per additional invoice.

The version cannot be pulled out because then it would not be covered by the checksum.

I think the vast majority of payment requests will only have a single destination anyways. The support for multiple destinations was added in response to this pull request, which hasn't been merged yet.

This is how the encoding might look at the machine level

This is how it looks at the software level. The most significant bits are encoded first (see RFC 3548). Displacing the least significant bits of e[15] offers the best compatibility with the existing base32 codebase. The serialization layer doesn't care that e is a little-endian integer. It just sees a sequence of octets.

@UkoeHB
Copy link

UkoeHB commented Dec 31, 2022

Why? The invoice version only affects the tagged fields and the checksum, which are not meant to be human-readable.

I'm thinking about manually debugging or sorting old URIs. Idk much about how these strings would/will be used in practice.

This is how it looks at the software level. The most significant bits are encoded first (see RFC 3548).

Ok this is a good reason. You may want to make a note of it to avoid future questions.

@rbrunner7
Copy link

About invoices:

PATH = INVOICE [ "+" INVOICE ...]

Isn't the possibility of having more than 1 invoice a bit overkill? I think with a maximum length of 994 characters this will top out anyway at 3 invoices already, and a QR code for that may be a monstrosity as well ...

I have read the PR #8665 that you linked and the possible uses, but I think everything is so much longer here that we bump against hard limits.

About version numbers:

I ask myself whether we couldn't simply take a single Base32 character for it. Because I would be very surprised if this even reaches version 3 sometime in the future, thus 30 possible versions look pretty safe. This "posvarint-16" encoding may be genius and cool, but if we don't need it, why include it in the first place?

@UkoeHB
Copy link

UkoeHB commented Jan 1, 2023

@rbrunner7 the posvarint-16 encoding is also used for tagged field lengths.

@tevador
Copy link
Author

tevador commented Jan 1, 2023

I think with a maximum length of 994 characters

This is the limit for one invoice. Multiple invoices can exceed it (each invoice has its own checksum). However, the practical limit for QR codes is not much higher than that anyways.

I'm not opposed to the removal of the multi-invoice feature if it's deemed unnecessary.

I ask myself whether we couldn't simply take a single Base32 character for it. Because I would be very surprised if this even reaches version 3 sometime in the future, thus 30 possible versions look pretty safe.

For versions 1-16, the posvarint is also a single character. I don't see any downsides to this solution. Btw, the CryptoNote block version is also a varint and having > 255 versions might seems like an overkill to someone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment