Skip to content

Instantly share code, notes, and snippets.

@tevador
Last active May 22, 2024 21:58
Show Gist options
  • Save tevador/50160d160d24cfc6c52ae02eb3d17024 to your computer and use it in GitHub Desktop.
Save tevador/50160d160d24cfc6c52ae02eb3d17024 to your computer and use it in GitHub Desktop.

JAMTIS

This document describes a new addressing scheme for Monero.

Chapters 1-2 are intended for general audience.

Chapters 3-7 contain technical specifications.

Table of Contents

1. Introduction

1.1 Why a new address format?

Sometime in 2024, Monero plans to adopt a new transaction protocol called Seraphis [1], which enables much larger ring sizes than the current RingCT protocol. However, due to a different key image construction, Seraphis is not compatible with CryptoNote addresses. This means that each user will need to generate a new set of addresses from their existing private keys. This provides a unique opportunity to vastly improve the addressing scheme used by Monero.

1.2 Current Monero addresses

The CryptoNote-based addressing scheme [2] currently used by Monero has several issues:

  1. Addresses are not suitable as human-readable identifiers because they are long and case-sensitive.
  2. Too much information about the wallet is leaked when scanning is delegated to a third party.
  3. Generating subaddresses requires view access to the wallet. This is why many merchants prefer integrated addresses [3].
  4. View-only wallets need key images to be imported to detect spent outputs [4].
  5. Subaddresses that belong to the same wallet can be linked via the Janus attack [5].
  6. The detection of outputs received to subaddresses is based on a lookup table, which can sometimes cause the wallet to miss outputs [6].

1.3 Jamtis

Jamtis is a new addressing scheme that was developed specifically for Seraphis and tackles all of the shortcomings of CryptoNote addresses that were mentioned above. Additionally, Jamtis incorporates two other changes related to addresses to take advantage of this large upgrade opportunity:

  • A new 16-word mnemonic scheme called Polyseed [7] that will replace the legacy 25-word seed for new wallets.
  • The removal of integrated addresses and payment IDs [8].

2. Features

2.1 Address format

Jamtis addresses, when encoded as a string, start with the prefix xmra and consist of 196 characters. Example of an address: xmra1mj0b1977bw3ympyh2yxd7hjymrw8crc9kin0dkm8d3wdu8jdhf3fkdpmgxfkbywbb9mdwkhkya4jtfn0d5h7s49bfyji1936w19tyf3906ypj09n64runqjrxwp6k2s3phxwm6wrb5c0b6c1ntrg2muge0cwdgnnr7u7bgknya9arksrj0re7whkckh51ik

There is no "main address" anymore - all Jamtis addresses are equivalent to a subaddress.

2.1.1 Recipient IDs

Jamtis introduces a short recipient identifier (RID) that can be calculated for every address. RID consists of 25 alphanumeric characters that are separated by underscores for better readability. The RID for the above address is regne_hwbna_u21gh_b54n0_8x36q. Instead of comparing long addresses, users can compare the much shorter RID. RIDs are also suitable to be communicated via phone calls, text messages or handwriting to confirm a recipient's address. This allows the address itself to be transferred via an insecure channel.

2.2 Light wallet scanning

Jamtis introduces new wallet tiers below view-only wallet. One of the new wallet tiers called "FindReceived" is intended for wallet-scanning and only has the ability to calculate view tags [9]. It cannot generate wallet addresses or decode output amounts.

View tags can be used to eliminate 99.6% of outputs that don't belong to the wallet. If provided with a list of wallet addresses, this tier can also link outputs to those addresses. Possible use cases are:

2.2.1 Wallet component

A wallet can have a "FindReceived" component that stays connected to the network at all times and filters out outputs in the blockchain. The full wallet can thus be synchronized at least 256x faster when it comes online (it only needs to check outputs with a matching view tag).

2.2.2 Third party services

If the "FindReceived" private key is provided to a 3rd party, it can preprocess the blockchain and provide a list of potential outputs. This reduces the amount of data that a light wallet has to download by a factor of at least 256. The third party will not learn which outputs actually belong to the wallet and will not see output amounts.

2.3 Wallet tiers for merchants

Jamtis introduces new wallet tiers that are useful for merchants.

2.3.1 Address generator

This tier is intended for merchant point-of-sale terminals. It can generate addresses on demand, but otherwise has no access to the wallet (i.e. it cannot recognize any payments in the blockchain).

2.3.2 Payment validator

This wallet tier combines the Address generator tier with the ability to also view received payments (including amounts). It is intended for validating paid orders. It cannot see outgoing payments and received change.

2.4 Full view-only wallets

Jamtis supports full view-only wallets that can identify spent outputs (unlike legacy view-only wallets), so they can display the correct wallet balance and list all incoming and outgoing transactions.

2.5 Janus attack mitigation

Janus attack is a targeted attack that aims to determine if two addresses A, B belong to the same wallet. Janus outputs are crafted in such a way that they appear to the recipient as being received to the wallet address B, while secretly using a key from address A. If the recipient confirms the receipt of the payment, the sender learns that they own both addresses A and B.

Jamtis prevents this attack by allowing the recipient to recognize a Janus output.

2.6 Robust output detection

Jamtis addresses and outputs contain an encrypted address tag which enables a more robust output detection mechanism that does not need a lookup table and can reliably detect outputs sent to arbitrary wallet addresses.

3. Notation

3.1 Serialization functions

  1. The function BytesToInt256(x) deserializes a 256-bit little-endian integer from a 32-byte input.
  2. The function Int256ToBytes(x) serialized a 256-bit integer to a 32-byte little-endian output.

3.2 Hash function

The function Hb(k, x) with parameters b, k, refers to the Blake2b hash function [10] initialized as follows:

  • The output length is set to b bytes.
  • Hashing is done in sequential mode.
  • The Personalization string is set to the ASCII value "Monero", padded with zero bytes.
  • If the key k is not null, the hash function is initialized using the key k (maximum 64 bytes).
  • The input x is hashed.

The function SecretDerive is defined as:

SecretDerive(k, x) = H32(k, x)

3.3 Elliptic curves

Two elliptic curves are used in this specification:

  1. Curve25519 - a Montgomery curve. Points on this curve include a cyclic subgroup 𝔾1.
  2. Ed25519 - a twisted Edwards curve. Points on this curve include a cyclic subgroup 𝔾2.

Both curves are birationally equivalent, so the subgroups 𝔾1 and 𝔾2 have the same prime order ℓ = 2252 + 27742317777372353535851937790883648493. The total number of points on each curve is 8ℓ.

3.3.1 Curve25519

Curve25519 is used exclusively for the Diffie-Hellman key exchange [11].

Only a single generator point B is used:

Point Derivation Serialized (hex)
B generator of 𝔾1 0900000000000000000000000000000000000000000000000000000000000000

Private keys for Curve25519 are 32-byte integers denoted by a lowercase letter d. They are generated using the following KeyDerive1(k, x) function:

  1. d = H32(k, x)
  2. d[31] &= 0x7f (clear the most significant bit)
  3. d[0] &= 0xf8 (clear the least significant 3 bits)
  4. return d

All Curve25519 private keys are therefore multiples of the cofactor 8, which ensures that all public keys are in the prime-order subgroup. The multiplicative inverse modulo is calculated as d-1 = 8*(8*d)-1 to preserve the aforementioned property.

Public keys (elements of 𝔾1) are denoted by the capital letter D and are serialized as the x-coordinate of the corresponding Curve25519 point. Scalar multiplication is denoted by a space, e.g. D = d B.

3.3.2 Ed25519

The Edwards curve is used for signatures and more complex cryptographic protocols [12]. The following three generators are used:

Point Derivation Serialized (hex)
G generator of 𝔾2 5866666666666666666666666666666666666666666666666666666666666666
U Hp("seraphis U") 126582dfc357b10ecb0ce0f12c26359f53c64d4900b7696c2c4b3f7dcab7f730
X Hp("seraphis X") 4017a126181c34b0774d590523a08346be4f42348eddd50eb7a441b571b2b613

Here Hp refers to an unspecified hash-to-point function.

Private keys for Ed25519 are 32-byte integers denoted by a lowercase letter k. They are generated using the following function:

KeyDerive2(k, x) = H64(k, x) mod ℓ

Public keys (elements of 𝔾2) are denoted by the capital letter K and are serialized as 256-bit integers, with the lower 255 bits being the y-coordinate of the corresponding Ed25519 point and the most significant bit being the parity of the x-coordinate. Scalar multiplication is denoted by a space, e.g. K = k G.

3.4 Block cipher

The function BlockEnc(s, x) refers to the application of the Twofish [13] permutation using the secret key s on the 16-byte input x. The function BlockDec(s, x) refers to the application of the inverse permutation using the key s.

3.5 Base32 encoding

"Base32" in this specification referes to a binary-to-text encoding using the alphabet xmrbase32cdfghijknpqtuwy01456789. This alphabet was selected for the following reasons:

  1. The order of the characters has a unique prefix that distinguishes the encoding from other variants of "base32".
  2. The alphabet contains all digits 0-9, which allows numeric values to be encoded in a human readable form.
  3. Excludes the letters o, l, v and z for the same reasons as the z-base-32 encoding [14].

4. Wallets

4.1 Wallet parameters

Each wallet consists of two main private keys and a timestamp:

Field Type Description
km private key wallet master key
kvb private key view-balance key
birthday timestamp date when the wallet was created

The master key km is required to spend money in the wallet and the view-balance key kvb provides full view-only access.

The birthday timestamp is important when restoring a wallet and determines the blockchain height where scanning for owned outputs should begin.

4.2 New wallets

4.2.1 Standard wallets

Standard Jamtis wallets are generated as a 16-word Polyseed mnemonic [7], which contains a secret seed value used to derive the wallet master key and also encodes the date when the wallet was created. The key kvb is derived from the master key.

Field Derivation
km BytesToInt256(polyseed_key) mod ℓ
kvb kvb = KeyDerive1(km, "jamtis_view_balance_key")
birthday from Polyseed

4.2.2 Multisignature wallets

Multisignature wallets are generated in a setup ceremony, where all the signers collectively generate the wallet master key km and the view-balance key kvb.

Field Derivation
km setup ceremony
kvb setup ceremony
birthday setup ceremony

4.3 Migration of legacy wallets

Legacy pre-Seraphis wallets define two private keys:

  • private spend key ks
  • private view-key kv

4.3.1 Standard wallets

Legacy standard wallets can be migrated to the new scheme based on the following table:

Field Derivation
km km = ks
kvb kvb = KeyDerive1(km, "jamtis_view_balance_key")
birthday entered manually

Legacy wallets cannot be migrated to Polyseed and will keep using the legacy 25-word seed.

4.3.2 Multisignature wallets

Legacy multisignature wallets can be migrated to the new scheme based on the following table:

Field Derivation
km km = ks
kvb kvb = kv
birthday entered manually

4.4 Additional keys

There are additional keys derived from kvb:

Key Name Derivation Used to
dfr find-received key kfr = KeyDerive1(kvb, "jamtis_find_received_key") scan for received outputs
dua unlock-amounts key kid = KeyDerive1(kvb, "jamtis_unlock_amounts_key") decrypt output amounts
sga generate-address secret sga = SecretDerive(kvb, "jamtis_generate_address_secret") generate addresses
sct cipher-tag secret ket = SecretDerive(sga, "jamtis_cipher_tag_secret") encrypt address tags

The key dfr provides the ability to calculate the sender-receiver shared secret when scanning for received outputs. The key dua can be used to create a secondary shared secret and is used to decrypt output amounts.

The key sga is used to generate public addresses. It has an additional child key sct, which is used to encrypt the address tag.

4.5 Key hierarchy

The following figure shows the overall hierarchy of wallet keys. Note that the relationship between km and kvb only applies to standard (non-multisignature) wallets.

key hierarchy

4.6 Wallet access tiers

Tier Knowledge Off-chain capabilities On-chain capabilities
AddrGen sga generate public addresses none
FindReceived dfr recognize all public wallet addresses eliminate 99.6% of non-owned outputs (up to § 5.3.5), link output to an address (except of change and self-spends)
ViewReceived dfr, dua, sga all view all received except of change and self-spends (up to § 5.3.14)
ViewAll kvb all view all
Master km all all

4.6.1 Address generator (AddrGen)

This wallet tier can generate public addresses for the wallet. It doesn't provide any blockchain access.

4.6.2 Output scanning wallet (FindReceived)

Thanks to view tags, this tier can eliminate 99.6% of outputs that don't belong to the wallet. If provided with a list of wallet addresses, it can also link outputs to those addresses (but it cannot generate addresses on its own). This tier should provide a noticeable UX improvement with a limited impact on privacy. Possible use cases are:

  1. An always-online wallet component that filters out outputs in the blockchain. A higher-tier wallet can thus be synchronized 256x faster when it comes online.
  2. Third party scanning services. The service can preprocess the blockchain and provide a list of potential outputs with pre-calculated spend keys (up to § 5.2.4). This reduces the amount of data that a light wallet has to download by a factor of at least 256.

4.6.3 Payment validator (ViewReceived)

This level combines the tiers AddrGen and FindReceived and provides the wallet with the ability to see all incoming payments to the wallet, but cannot see any outgoing payments and change outputs. It can be used for payment processing or auditing purposes.

4.6.4 View-balance wallet (ViewAll)

This is a full view-only wallet than can see all incoming and outgoing payments (and thus can calculate the correct wallet balance).

4.6.5 Master wallet (Master)

This tier has full control of the wallet.

4.7 Wallet public keys

There are 3 global wallet public keys. These keys are not usually published, but are needed by lower wallet tiers.

Key Name Value
Ks wallet spend key Ks = kvb X + km U
Dua unlock-amounts key Dua = dua B
Dfr find-received key Dfr = dfr Dua

5. Addresses

5.1 Address generation

Jamtis wallets can generate up to 2128 different addresses. Each address is constructed from a 128-bit index j. The size of the index space allows stateless generation of new addresses without collisions, for example by constructing j as a UUID [15].

Each Jamtis address encodes the tuple (K1j, D2j, D3j, tj). The first three values are public keys, while tj is the "address tag" that contains the encrypted value of j.

5.1.1 Address keys

The three public keys are constructed as:

  • K1j = Ks + kuj U + kxj X + kgj G
  • D2j = daj Dfr
  • D3j = daj Dua

The private keys kuj, kxj, kgj and daj are derived as follows:

Keys Name Derivation
kuj spend key extensions kuj = KeyDerive2(sga, "jamtis_spendkey_extension_u" || j)
kxj spend key extensions kxj = KeyDerive2(sga, "jamtis_spendkey_extension_x" || j)
kgj spend key extensions kgj = KeyDerive2(sga, "jamtis_spendkey_extension_g" || j)
daj address keys daj = KeyDerive1(sga, "jamtis_address_privkey" || j)

5.1.2 Address tag

Each address additionally includes an 18-byte tag tj = (j', hj'), which consists of the encrypted value of j:

  • j' = BlockEnc(sct, j)

and a 2-byte "tag hint", which can be used to quickly recognize owned addresses:

  • hj' = H2(sct, "jamtis_address_tag_hint" || j')

5.2 Sending to an address

TODO

5.3 Receiving an output

TODO

5.4 Change and self-spends

TODO

5.5 Transaction size

Jamtis has a small impact on transaction size.

5.5.1 Transactions with 2 outputs

The size of 2-output transactions is increased by 28 bytes. The encrypted payment ID is removed, but the transaction needs two encrypted address tags t~ (one for the recipient and one for the change). Both outputs can use the same value of De.

5.5.2 Transactions with 3 or more outputs

Since there are no "main" addresses anymore, the TX_EXTRA_TAG_PUBKEY field can be removed from transactions with 3 or more outputs.

Instead, all transactions with 3 or more outputs will require one 50-byte tuple (De, t~) per output.

6. Address encoding

6.1 Address structure

An address has the following overall structure:

Field Size (bits) Description
Header 30* human-readable address header (§ 6.2)
K1 256 address key 1
D2 255 address key 2
D3 255 address key 3
t 144 address tag
Checksum 40* (§ 6.3)

* The header and the checksum are already in base32 format

6.2 Address header

The address starts with a human-readable header, which has the following format consisting of 6 alphanumeric characters:

"xmra" <version char> <network type char>

Unlike the rest of the address, the header is never encoded and is the same for both the binary and textual representations. The string is not null terminated.

The software decoding an address shall abort if the first 4 bytes are not 0x78 0x6d 0x72 0x61 ("xmra").

The "xmra" prefix serves as a disambiguation from legacy addresses that start with "4" or "8". Additionally, base58 strings that start with the character x are invalid due to overflow [16], so legacy Monero software can never accidentally decode a Jamtis address.

6.2.1 Version character

The version character is "1". The software decoding an address shall abort if a different character is encountered.

6.2.2 Network type

network char network type
"t" testnet
"s" stagenet
"m" mainnet

The software decoding an address shall abort if an invalid network character is encountered.

6.3 Checksum

The purpose of the checksum is to detect accidental corruption of the address. The checksum consists of 8 characters and is calculated with a cyclic code over GF(32) using the polynomial:

x8 + 3x7 + 11x6 + 18x5 + 5x4 + 25x3 + 21x2 + 12x + 1

The checksum can detect all errors affecting 5 or fewer characters. Arbitrary corruption of the address has a chance of less than 1 in 1012 of not being detected. The reference code how to calculate the checksum is in Appendix A.

6.4 Binary-to-text encoding

An address can be encoded into a string as follows:

address_string = header + base32(data) + checksum

where header is the 6-character human-readable header string (already in base32), data refers to the address tuple (K1, D2, D3, t), encoded in 910 bits, and the checksum is the 8-character checksum (already in base32). The total length of the encoded address 196 characters (=6+182+8).

6.4.1 QR Codes

While the canonical form of an address is lower case, when encoding an address into a QR code, the address should be converted to upper case to take advantage of the more efficient alphanumeric encoding mode.

6.5 Recipient authentication

TODO

7. Test vectors

TODO

References

  1. https://github.com/UkoeHB/Seraphis
  2. https://github.com/monero-project/research-lab/blob/master/whitepaper/whitepaper.pdf
  3. monero-project/meta#299 (comment)
  4. https://www.getmonero.org/resources/user-guides/view_only.html
  5. https://web.getmonero.org/2019/10/18/subaddress-janus.html
  6. monero-project/monero#8138
  7. https://github.com/tevador/polyseed
  8. monero-project/monero#7889
  9. monero-project/research-lab#73
  10. https://eprint.iacr.org/2013/322.pdf
  11. https://cr.yp.to/ecdh/curve25519-20060209.pdf
  12. https://ed25519.cr.yp.to/ed25519-20110926.pdf
  13. https://www.schneier.com/wp-content/uploads/2016/02/paper-twofish-paper.pdf
  14. http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
  15. https://en.wikipedia.org/wiki/Universally_unique_identifier
  16. https://github.com/monero-project/monero/blob/319b831e65437f1c8e5ff4b4cb9be03f091f6fc6/src/common/base58.cpp#L157

Appendix A: Checksum

# Jamtis address checksum algorithm

# cyclic code based on the generator 3BI5PLC1
# can detect 5 errors up to the length of 994 characters
GEN=[0x1ae45cd581, 0x359aad8f02, 0x61754f9b24, 0xc2ba1bb368, 0xcd2623e3f0]

M = 0xffffffffff

def jamtis_polymod(data):
    c = 1
    for v in data:
        b = (c >> 35)
        c = ((c & 0x07ffffffff) << 5) ^ v
        for i in range(5):
            c ^= GEN[i] if ((b >> i) & 1) else 0
    return c

def jamtis_verify_checksum(data):
    return jamtis_polymod(data) == M

def jamtis_create_checksum(data):
    polymod = jamtis_polymod(data + [0,0,0,0,0,0,0,0]) ^ M
    return [(polymod >> 5 * (7 - i)) & 31 for i in range(8)]

# test/example

CHARSET = "xmrbase32cdfghijknpqtuwy01456789"

addr_test = (
    "xmra1mj0b1977bw3ympyh2yxd7hjymrw8crc9kin0dkm8d3"
    "wdu8jdhf3fkdpmgxfkbywbb9mdwkhkya4jtfn0d5h7s49bf"
    "yji1936w19tyf3906ypj09n64runqjrxwp6k2s3phxwm6wr"
    "b5c0b6c1ntrg2muge0cwdgnnr7u7bgknya9arksrj0re7wh")

addr_data = [CHARSET.find(x) for x in addr_test]
addr_enc = addr_data + jamtis_create_checksum(addr_data)
addr = "".join([CHARSET[x] for x in addr_enc])

print(addr)
print("len =", len(addr))
print("valid =", jamtis_verify_checksum(addr_enc))
@j-berman
Copy link

Eh, ya, I'm gonna take a harder stance on this. I think the address type header should definitely be removed, and certified addresses should not be part of the design consideration for JAMTIS at all.

@tevador
Copy link
Author

tevador commented Jan 28, 2022

@j-berman

Eh, ya, I'm gonna take a harder stance on this. I think the address type header should definitely be removed, and certified addresses should not be part of the design consideration for JAMTIS at all.

Can you elaborate what exactly the problem would be with certified addresses? AFAICS a certified Jamtis address conveys roughly the same information as a current integrated address. Integrated addresses are still used by merchants and one reason for that is customer assurance (customers can be more certain it's the correct address because they've seen it before). Since Jamtis doesn't support integrated address, this functionality is provided by certified addresses.

Certified addresses are in line with the design philosophy of Monero: privacy by default. All Jamtis addresses are unlinkable by default and can be optionally linked to the wallet if the owner wishes so.

Certified addresses are not meant for regulatory purposes. If you give someone a certified address, it doesn't actually prove that you own that address. It only proves that the address is owned by a specific wallet. In order to prove ownership of the private keys, you'd have to provide a signature of some challenge string (this seems to be what the AOPP protocol is doing). The CLI/GUI wallets already offer the option to sign arbitrary messages with your private keys, so this is actually way more likely to be used by regulators.

This tier doesn't reduce data a light wallet has to download. Light wallets currently only download outputs sent to the user, and the transactions of outputs plausibly spent by the user.

By "light wallet" I mean a wallet using a 3rd party daemon (not sure if there is another term for that).

I disagree with this framing, unless you mean to suggest that light wallets will replace full scanning wallets, which I would disagree with. Rather they will have an improved impact on privacy, and a limited UX impact.

Again, probably wrongly phrased. The privacy impact is limited in comparison to wallets that hold your view key. UX improvement is relative to a full wallet (running your own daemon).

Can you provide a bit more color here why the most significant 8 bits should be set to zero?

It's a quick way to check that the output is not yours. If the most significant 8 bits are not all zero, it means this can't be your address and you can skip deriving the output spend key.

@UkoeHB

Let the sender-receiver secret be q = H_32(...) instead of H_s(...)

That's a good idea.

In 2-out transactions, I want to only use one enote ephemeral key

This is already mentioned in section 5.5.1.

@Tigerix

Thus I am strongly voting for this:
"It would be much more human readable if the RID was, say, 4 words from a 2048-word dictionary (like BIP39), so that instead of the difficult-to-read h8eug-w77qs-aaf7m-ww63i-hn33c you would get correct-horse-battery-coffee"

This has already been discussed. 4 words are insecure, you'd need about 11 words for security reasons and that might get confused with a wallet seed.

@j-berman
Copy link

j-berman commented Jan 29, 2022

With more thought, I'd say I was too hasty to jump to a strong stance against certified addresses.

Repeating some thoughts from Matrix/IRC:

Certified addresses go a step further than reusing a primary or integrated address because you're providing a proof of ownership.

If I give you a certified address, and later I give Bob a certified address, you and Bob have cryptographic proof that I'm in control of the address on separate occasions.

If I give you my primary or integrated address, and I give Bob the same address later, you and Bob don't have cryptographic proof I'm in control of the address.

In the latter, I have plausible deniability that I'm still in control the address. In the former I do not.

I agree a critical difference to highlight between the certified address scheme and AOPP is that in AOPP you sign a message produced by the counter-party. Whereas with certified addresses, you just sign the address in your possession. And the bad part about AOPP is the content of the message being signed and its purpose.

From a privacy perspective, I don't think certified addresses are awful and I'll weaken my stance. I agree that arbitrary message signing in the client is a clearer path toward regulatory concerns in the same vein as AOPP, but I wouldn't argue for the removal of arbitrarh message signing as it's clear what you're doing when you sign a message and provide someone else a signature. However, I do think certified addresses offer a clearer, and stronger path toward linking identity to wallet identity to address, as part of the address protocol, and in a way that is not as obvious as "hey, sign this message". I don't think a slippery slope argument is strong enough to argue for their exclusion, so I concede my position isn't defended well enough to warrant their exclusion.

Perhaps it's already an acceptable default assumption that once you give someone your address, you are comfortable with them (and anyone else you've given the address to) knowing that you control that address, so a proof is not necessarily changing what users already expect in their behavior. The only major issue I can concretely identify is one of user education, in making sure users who generate new certified addresses are aware that anyone they've given any of their certified addresses to in the past could link the two different certified addresses. And your RID idea would solve this satisfactorily if widely used and recipients understood what they are.


By "light wallet" I mean a wallet using a 3rd party daemon (not sure if there is another term for that).
Again, probably wrongly phrased. The privacy impact is limited in comparison to wallets that hold your view key. UX improvement is relative to a full wallet (running your own daemon).

This is more of semantics at this point, but explaining my view:

I use "light wallet" = wallet client points to a light wallet server centrally hosted that scans the chain server-side
and "full scanning wallet" = wallet client points to a daemon, either 3rd party or self-hosted, and wallet scans the chain client-side

I think the light wallet scheme defined in JAMTIS is a privacy upgrade for light wallets today (at minor cost of UX), and is useful in reducing the threat of the seemingly inevitable not-perfectly-ideal scenario that is a large centrally hosted light wallet server. But the privacy gap between a JAMTIS light wallet that points to a 3rd party server compared to a JAMTIS full scanning wallet that points to a 3rd party daemon is large. It's more difficult for the 3rd party daemon to determine received outs since 1, address reuse doesn't reveal anything, and 2, the daemon can't determine view tag matched outputs.

To me, the ideal flow goes:
(1, the best) run your own infra and use a wallet that points to it
(2, worse) use a full scanning wallet -> 3rd party daemon,
(3, the worst) use a light wallet -> centralized 3rd party hosted server

I don't think it would be good if many stopped using (2) in favor of (3), and so I'd say the benefits/costs of this new JAMTIS "FindReceived" tier shouldn't be measured against (2), rather against (3). But alas, imo this is less important than the building of it and semantics aren't worth debating over, so I digress.

For the record I'd also include a (1.1) for running your own light wallet server but didn't want to complicate :)


It's a quick way to check that the output is not yours. If the most significant 8 bits are not all zero, it means this can't be your address and you can skip deriving the output spend key.

Ah, I think I see. Do I follow that this is what enables the if j >= 2^56 || 2^57, abort steps, which are like the view tag optimization, since they avoid the EC operations that follow?

@tevador
Copy link
Author

tevador commented Jan 29, 2022

Certified addresses go a step further than reusing a primary or integrated address because you're providing a proof of ownership.

No, certified addresses don't prove ownership.

Imagine the following scenario:

Eve's exchange requires withdrawals to certified addresses. Alice can go to Bob's online shop, which gives certified addresses to its customers, and makes an order. She can withdraw from her exchange account into the certified address provided by Bob, paying for her order in the process. Eve holds no proof who the owner of the address is (but she will be able to link the addresses owned by Bob if multiple people pay for their orders using Eve's exchange). In this regard, certified addresses are no different from integrated addresses.

To prove ownership of a certified address, you'd also need to prove the ownership of the private key kid, which would require signing some challenge message provided by the exchange.

@j-berman
Copy link

Alice returns to the exchange 1 month later with another one of Bob's certified addresses. The exchange gains stronger knowledge that Bob is likely still in control of the same address, because it is a fresh address that is signed with the same key.

Now assume Bob had used an integrated address instead, and Alice returns to the exchange 1 month later with the same integrated address as she used the first time around. The exchange has evidence Bob is likely still in control of this integrated address, but the evidence is not as strong since it is not accompanied by a digital signature. Bob has a greater degree of plausible deniability that he is still in control of the integrated address in this circumstance.

@j-berman
Copy link

Assume Fred is a data broker and wants to harvest all the data in the world so he can sell it. Fred will pay more for outgoing payments to cryptographically signed addresses, because they make it marginally more difficult to fake the economic action that took place, and are thus marginally more likely to be honest payments, aka good data. Eve can make payments all they want out to the same integrated addresses with no interaction from counter-parties. But Eve can't fake counter-parties' signatures.

It is a subtle difference; integrated addresses and certified addresses are not equivalent.

@j-berman
Copy link

j-berman commented Jan 29, 2022

An idea: an address where one of the public keys stays constant, but another public key is derived from j. This would allow:

  • a unified address type that all wallets support outgoing payments to by default
  • batched outgoing payments to this type of address
  • a merchant to use a constant public key so repeat customers can trust the address after first visit
  • a merchant to use a counter for unique payment receipts
  • we can deprecate payment ID's = saves some space on chain

Possible? I'm working on my math I promise

EDIT: I do see how there is still room for mistyping/copying or a grieving vector where a MITM gives you the right public address but other keys are invalid. And so it doesn't perfectly solve those issues. But another address type to consider that has attractive properties I think. RID's to the rescue...

And I figure this would make it very difficult to have unified scanning logic. But maybe that is ok? A merchant wallet could be a totally separate type of wallet. It would be easier for other Monero wallet devs who aren't concerned with implementing a merchant wallet to not have to worry about payments to different types of addresses, which is imo a much more significant benefit. Merchant wallets will likely have different UX expectations than regular user-facing wallets anyway.

Merchant wallets should really look and feel and function more like accounting software, whereas user-facing wallets should function more like Venmo UX-wise (as they currently do). It makes perfect sense imo to split development along those lines, and for internal logic to be different across both, but both are perfectly interoperable with each other without extra steps like supporting multiple address types in outgoing payments.

@tevador
Copy link
Author

tevador commented Jan 29, 2022

The exchange gains stronger knowledge that Bob is likely still in control of the same address

I don't agree with this conclusion.

  1. Certified addresses do not contain a timestamp, so it's impossible to determine when the signature was made.
  2. The private key used to sign certified addresses is different from the private key needed to spend, so someone might be "in control" of a wallet in terms of generating addresses, yet unable to spend the received funds.

But Eve can't fake counter-parties' signatures.

Anyone can take an anonymous Jamtis address and turn it into a "certified" address by attaching a signature with an arbitrary key. I could sell Fred as many fake payments as I want to any number of fake recipients. In reality, all the transactions go back to my wallet.

A merchant wallet could be a totally separate type of wallet

We had this in an earlier version of Jamtis, but it was deemed bad for UX and slightly problematic for migrating legacy wallets (you could migrate a wallet twice and get two different sets of addresses).

@j-berman
Copy link

j-berman commented Jan 29, 2022

  1. Certified addresses do not contain a timestamp, so it's impossible to determine when the signature was made.

In the real world, you build up pieces of evidence in a case to support a position. It is evidence, and stronger evidence than one without a signature. Just because it does not contain a timestamp does not discount its admissibility or usefulness as evidence to form a conclusion. A timestamp can be faked too, does that then discount the timestamp as useful evidence? No it does not. The primary reason a user benefits from their existence (knowing they're sending to the same person who controls the wallet EDIT: at that point in time) is the same reason they are also useful as evidence for other purposes.

  1. The private key used to sign certified addresses is different from the private key needed to spend, so someone might be "in control" of a wallet in terms of generating addresses, yet unable to spend the received funds.

Same as above. This doesn't negate their usefulness as evidence to form a conclusion that is stronger than what one can form from integrated addresses.

We had this in an earlier version of Jamtis, but it was deemed bad for UX and slightly problematic for migrating legacy wallets (you could migrate a wallet twice and get two different sets of addresses).

I propose we hear more opinions and re-consider it :) Was there an address scheme in the earlier version like this too? EDIT: here it is. Reading through it again to think on it with a better idea of the tradeoffs and goals

@j-berman
Copy link

j-berman commented Jan 29, 2022

Here, put another way:

LEO has evidence Eve, Alice, and Bob were communicating at a particular point in time t1, and at time t2. LEO can build a stronger case that proves this happened if they have 2 of Bob's signed certified addresses recovered from those points of communication, versus if they were just integrated addresses, since integrated addresses aren't signed by Bob.

@j-berman
Copy link

Anyone can take an anonymous Jamtis address and turn it into a "certified" address by attaching a signature with an arbitrary key. I could sell Fred as many fake payments as I want to any number of fake recipients. In reality, all the transactions go back to my wallet.

I could wash trade as many credit card transactions as I want and sell that data to data providers. This doesn't negate the economics that exists of extremely valuable data collection industries. Signatures make the data marginally more valuable, though yes, things can still be faked.

@UkoeHB
Copy link

UkoeHB commented Jan 29, 2022

@tevador I am going to change the view tag computation (for Jamtis). From: v = H1("view tag", Kd). To: v = H1("view tag", Kd, Ko). This fixes an edge case Janus attack.

I removed output indices from output computations, to allow 'make an enote in isolation then chain a tx off it'. Suppose A) someone makes an enote sending money to me, B) I make a tx funding just that enote (so there will be that enote + a change enote). Both of the outputs in that tx will have the same view tag, because there will be the same derived key K_d (due to the efficiency magic around 2-out txs). One thing you could do is, if there is one non-change output, always check if it is a 'normal self-send' (i.e. not a special self-send, just a normal enote that happens to be owned by you). But this check opens up a Janus attack.

If the enote in step (A) weren't sent to me, then most of the time it wouldn't have the same view tag as my change output. An attacker could test if an address is mine by making isolated enotes, getting me to fund them, then checking if the view tags match the change outputs (via me rejecting/accepting the proposed isolated enotes).

Adding Ko to view tags solves that because Jamtis change enotes and normal enotes always have different onetime addresses (even if the underlying destination address is the same).

Note that 'self-spend' enote types (non-change self-sends) will only be created by a tx author, so they can't be used for a Janus attack (hence it is safe to rely on a check is_self_send_proposal() when deciding how to create a change output). On second thought, there is one tiny edge case that allows Janus: if you make an isolated enote as a self-send, give it to someone else anonymously, then that person gives it back to you to fund (using the appearance of a 3-out vs 2-out tx as indication if the isolated enote was a self-send that you created). However, you shouldn't hand out self-send enotes that you might not fund, since if you don't fund them then you will never find them in the chain.

Note: An alternative solution would be 'if someone gives you a single isolated enote, then always add a dummy enote'. However, that isn't super robust, and reduces privacy by preventing you from hiding isolated enotes in the more-common 2-out pool.

@busyboredom
Copy link

I propose we hear more opinions and re-consider it :)

My opinion is that this should be a UI issue.

  • Software with a focus on non-merchant use (like Cake Wallet) should default to anonymous addresses and hide certified addresses in an "advanced" menu (or just not support them at all).
  • Software with a focus on merchant use (like the WooCommerce plugin and whatnot) should default to certified addresses and should warn the merchant about their linkability.

No need for separate wallet types in Jamtis, it can just be a UI thing. I do agree with the spirit though -- casual users don't need to see the option to use certified addresses unless they're looking hard for it.

@j-berman
Copy link

j-berman commented Jan 30, 2022

I think everyone is under-appreciating these 3 very negative tradeoffs that certified addresses make:

  • every single Monero wallet, 3rd party wallets included, has to develop software that specifically supports sending to both certified addresses AND anonymous addresses
  • users will likely need to learn and understand why and how certified are different from normal addresses = cognitive overhead for customers at the point of sale, one of the merchant's most critical points (and the most critical point Monero is supposed to provide an excellent experience for)
  • they reduce plausible deniability that you control an address. I don't agree with @tevador 's arguments, but, it appears we will agree to disagree on that.

I think the idea to create an address type that is not different from any other regular address and does not have the above tradeoffs, but retains the benefits merchants apparently want, is far superior.

If the main concern is development concern and a blow-up of features that people have to support, I am happy to take it on myself and implement it totally separately and have all code live separate from the user-facing wallet code i.e. implement the merchant wallet. There shouldn't be anything that prevents this. In fact I might even do it if certified addresses also exist because I feel that strongly. But by choosing to go with certified addresses as the solution here, we are putting a larger burden on every Monero wallet developer ever.


No need for separate wallet types in Jamtis

Technically you could still have one wallet that supports both. But the complexity and development resources needed with this alternative address type to certified addresses is larger (I'm gonna name this alternative "merchant addresses").

From a dev standpoint, I think it's a much more prudent decision to allow for the wallet types to be silo'd and allow wallet devs to choose to support their desired UX based on expected users, rather than require every single wallet dev support an extra feature (sending to certified addresses).

I.e. anyone can choose add the merchant address feature to their wallet so users can receive payments to merchant addresses if they want to, but the likely maintenance and dev resource cost would be large. Versus certified addresses, which require ALL wallet devs to implement sending to certified addresses, which isn't as complex to implement as implementing the ability to receive to merchant addresses, but still requires resources to implement and maintain and build a UX for.

@tevador
Copy link
Author

tevador commented Jan 30, 2022

It is evidence, and stronger evidence than one without a signature.

I don't agree that certified addresses provide meaningfully more evidence than reused integrated addresses. So we can agree to disagree on that.

However, I seem to have convinced you that certified addresses don't prove ownership, so that's at least one thing we can agree on.

every single Monero wallet, 3rd party wallets included, has to develop software that specifically supports sending to both certified addresses AND anonymous addresses

Sending to a certified address works exactly the same way as sending to an anonymous address. The only thing that is different is the RID calculation. I estimate that the support for sending to certified addresses would be under 20 lines of code.

Having this feature included in the official specs means that merchants can rely on it being widely supported. If this was some 3rd party extension, it would be unusable in practice.

users will likely need to learn and understand why and how certified are different from normal addresses

No, they will not. Users will only need to care about the RID. If we wanted to prevent users from shooting themselves in the foot by using a certified address, we could restrict their generation to the wallet RPC interface.

I would appreciate some feedback from an actual merchant about certified addresses. If merchants say they don't need this feature, we can remove it.

@j-berman
Copy link

j-berman commented Jan 30, 2022

The only thing that is different is the RID calculation. I estimate that the support for sending to certified addresses would be under 20 lines of code.

It requires a dev to learn what they are, then find the relevant code to write, and to make sure their UI can handle larger (and variable length) address strings nicely, write tests for both. But sure I would estimate it at a few hours of work tops maybe less.

Users will only need to care about the RID.

You can't avoid copy pasting addresses in all cases. Having one address 50% longer than another is an oddity that takes thought to understand and not find strange.

we could restrict their generation to the wallet RPC interface

Ok that's something

If merchants say they don't need this feature, we can remove it.

I'm not arguing for removal of a feature that would enable what merchants want, I'm arguing for an alternative. Can you explain why you are against the alternative?

(For the record I am also a merchant, service I helped start accepts Monero)

@tevador
Copy link
Author

tevador commented Jan 30, 2022

Can you explain why you are against the alternative?

I haven't seen an alternative proposal, but I'm assuming you mean the return to the previous version of the specs which had a separate merchant wallet type. This wallet type would derive the address key K1 and the address tag t the same way as "non-merchant" wallets, but the other two keys K2 and K3 would be constant. RID would be derived by hashing only K2 and K3.

Advantages:

  • Uniform address length
  • Marginally simpler code on the sending side
  • The disputable privacy benefit of not signing anything. I claim that there is no difference.

Disadvantages:

  • Users would have to understand the difference between the wallet types and decide at wallet generation time if they want privacy or customer assurance. This could be somewhat mitigated by restricting the merchant wallet type to the wallet RPC interface, but merchant wallets would still offer strictly worse privacy.
  • Possible issues with legacy wallets. The same wallet could be restored once as a merchant wallet and later as a non-merchant wallet, causing two different sets of addresses to be produced.
  • More complex address and wallet generation code
  • The assurance provided by RIDs would be significantly weakened because an attacker could replace K1 or t without changing the RID. This might cause the funds sent to the modified address to be permanently lost, which is something that cannot happen with an integrated address or the current Jamtis proposal.

The complete removal of certified addresses from the current proposal would have the same advantages but none of the disadvantages (the only disadvantage would be the loss of customer assurance). The above proposal seems to be the worst of the 3 options.

@busyboredom
Copy link

busyboredom commented Jan 30, 2022

I can think of one alternative that I just put on matrix and now realize that this is a better place to say it:

Gingeropolous floated the idea of a txpool-only "payload system" earlier: https://gist.github.com/tevador/50160d160d24cfc6c52ae02eb3d17024?permalink_comment_id=4044233#gistcomment-4044233

It sounds similar to the chat system used in Townforge. A nice alternative to messaging over tx_extra.

Merchants could put something like "DON'T EDIT: f6kfd984jdls9" as the payload to link the transaction to a given invoice, allowing the merchant to reuse a single anonymous address instead of needing to generate a bunch of certified addresses. We'd get a unified address type and ephemeral messaging at the same time. It does seem quite a bit more complicated than certified addresses though, and would require merchants to see the message before expiration (so if their server went down, they'd miss the payment). Not sure if it's the best alternative, but it is an alternative.

@j-berman
Copy link

j-berman commented Jan 30, 2022

These disadvantages are part of why I argue it would be reasonable to silo merchant wallet types from normal user wallet types. A user who uses Venmo doesn't need to learn what QuickBooks is for example. I disagree it is the worst of the 3 options, and those advantages are significant.

I think the 4th disadvantage of weaker RID is of marginal concern in line with the weakened plausible deniability.

But I think we have fairly examined and highlighted the tradeoffs at this point, and have fully shared our opinions.

I concur more feedback from others would be useful to move forward from here.

@j-berman
Copy link

Also, the RID could still be a hash of all 3 pub keys, but clients can recognize the same constant pub keys via a contact they store after first visit.

@tevador
Copy link
Author

tevador commented Jan 30, 2022

I think the 4th disadvantage of weaker RID is of marginal concern

Another thing we disagree on. I find the 4th disadvantage the most concerning, up to the point of making RIDs essentially useless for the purpose they were designed to do. The DNS/onion/manual verification would make little sense if it only covered part of the address payload.

I would prefer to remove certified addresses than destroy the anonymous functionality provided by RIDs. The merchant wallet type could then be implemented independently as a 3rd party extension. Users would just have to compare part of the address instead of the RID to get some level of assurance.

@j-berman
Copy link

@busyboredom there would still need to be a protocol where merchant gives extra info to customer, customer has to pass that extra info into wallet, wallet has to parse and send the tx and the extra info to the messaging network. So it's effectively a different address type like integrated addresses, but without storing extra data on chain and instead sending to the messaging network. I think integrated addresses would be my preference over that because in this case the ephemeral nature of it is a pretty significant downside.

@j-berman
Copy link

Fair enough @tevador

I apologize for some of the testier comments in my debating. I think you make reasonable points, we just disagree on relative costs and benefits 🤝

@busyboredom
Copy link

effectively a different address type

I wouldn't consider invoices to be a separate address type. Invoices already exist and simply provide relevant information (like amount due) along with the address. Currently, they're even human readable. The only change here is that the "tx_description" field would be sent to the txpool when the transaction is sent. Here's an example of an invoice I just generated using the GUI wallet: monero:4A1WSBQdCbUCqt3DaGfmqVFchXScF43M6c5r4B6JXT3dUwuALncU9XTEnRPmUMcB3c16kVP9Y7thFLCJ5BaMW3UmSy93w3w?tx_amount=1&tx_description=for%20pizza&recipient_name=John%20Smith

I think integrated addresses would be my preference over that because in this case the ephemeral nature of it is a pretty significant downside.

I believe the ephemeral nature is justified by reduced blockchain bloat compared to integrated addresses, but it is certainly worth debating.

You are definitely correct that needing to send the description to the txpool introduces some complexity though, that's a very fair criticism. Some new logic would need to be added to both the wallet and daemon. If there is a simpler alternative that doesn't take up space on the blockchain, I'd be on board with it.

@tevador
Copy link
Author

tevador commented Jan 31, 2022

In the view tag PR, @knaccc brought up an interesting point that curve25519 x-coordinate DH exchange seems to be significantly faster than ed25519 key exchange.

I think this is definitely something we should investigate for jamtis. We could modify addresses so that the public keys K2 and K3 only encode the x-coordinate of the Montgomery curve points. This would speed up output scanning for two reasons:

  1. No need to recover the second coordinate from the compressed point (this typically requires a slow field inversion square root operation).
  2. x-coordinate scalar multiplication seems to be much faster because the y-coodinate operations can be skipped

@UkoeHB
Copy link

UkoeHB commented Jan 31, 2022

@tevador I didn't think that far, good idea. In that case, {K_2, K_3, K_e, K_d, r G} can all be in x25519. Maybe if @j-berman gets a demo running, then I can use it to adapt my implementation to demo this.

@UkoeHB
Copy link

UkoeHB commented Jan 31, 2022

@tevador If only K_d is in x25519 for view tags only (K_d for computing q remains in ed25519), then it is possible for implementations to not support x25519 and just do slow scanning. This might be advantageous.

@tevador
Copy link
Author

tevador commented Feb 1, 2022

I ran some preliminary benchmarks using the amd64_64_24k assembly implementation from SUPERCOP on my Ryzen 3700X.

operation clock cycles
x25519_varbase_scalarmult 138500
ed25519_varbase_scalarmult 155200

So the speed up is not 2x as previously reported but still significant 12%. Note that this is for a 64-bit assembly implementation. The difference may be larger with the ref10 implementation.

In both cases, the above figures include point deserialization, variable base scalar multiplication and point serialization. However, it seems that the main advantage of x25519 is the point deserialization. If I remove the call to ge25519_unpackneg_vartime from the ed25519 benchmark, it runs in 140600 clock cycles - almost the same performance as x25519.

The conclusion is that if we want to take advantage of the x25519 speed up, transactions must contain Ke serialized in x25519 format (x-coordinate only) and not ed25519 compressed points. Decompressing an ed25519 point and converting it to x25519 would most likely be slower than just using ed25519 scalar multiplication as we currently do.

@j-berman
Copy link

j-berman commented Feb 1, 2022

@tevador any shot your code is in a shareable state so I can take a look/learn from it please :)

@UkoeHB
Copy link

UkoeHB commented Feb 1, 2022

@tevador You can cheaply convert ed25519 compressed points to x25519 like this:

int
crypto_sign_ed25519_pk_to_curve25519_remove_extra_ops(unsigned char *curve25519_pk,
                                                      const unsigned char *ed25519_pk)
{
    fe25519    original_y;
    fe25519    x;
    fe25519    one_minus_y;

    // get y coordinate of ed25519 point
    unsigned char ed25519_pk_copy[32];
    memcpy(ed25519_pk_copy, ed25519_pk, 32);
    ed25519_pk_copy[31] &= UCHAR_MAX >> 1;
    fe25519_frombytes(original_y, ed25519_pk_copy);

    // ed25519 -> curve25519
    fe25519_1(one_minus_y);
    fe25519_sub(one_minus_y, one_minus_y, original_y);
    fe25519_1(x);
    fe25519_add(x, x, original_y);
    fe25519_invert(one_minus_y, one_minus_y);
    fe25519_mul(x, x, one_minus_y);
    fe25519_tobytes(curve25519_pk, x);

    return 0;
}

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