Skip to content

Instantly share code, notes, and snippets.

@tevador
Last active July 17, 2024 21:03
Show Gist options
  • Save tevador/d3656a217c0177c160b9b6219d9ebb96 to your computer and use it in GitHub Desktop.
Save tevador/d3656a217c0177c160b9b6219d9ebb96 to your computer and use it in GitHub Desktop.

JAMTIS-RCT

This document introduces a new addressing scheme for Monero called Jamtis. The new addresses are 244 characters long and come with several new features. The new scheme allows users to delegate blockchain scanning to a 3rd party service without revealing which specific outputs belong to the wallet or the amounts that were received. New wallet tiers are introduced for merchants that only have capabilities for generating addresses or processing incoming payments. New addresses can be created statelessly (without the need to keep track of how many addresses have been generated). View-only wallets can display the correct balance.

The transaction protocol that comes with Jamtis is backwards compatible with existing CryptoNote addresses. That means wallets can send payments to both new and old addresses and the resulting transactions will be indistinguishable in the blockchain. Additionally, the protocol provides Janus attack mitigations for both new and old addresses.

Table of Contents

1. Introduction

1.1 Why a new address format?

When Monero was created in 2014, it inherited the the CryptoNote addressing scheme [1]. Originally, each wallet only had a single public address and payments were disambiguated with payment IDs. In 2017, subaddresses were introduced, which allowed each wallet to generate a virtually unlimited number of seemingly unlinkable addresses. However, several issues with the legacy addressing scheme have been identified:

  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 [2].
  4. View-only wallets need key images to be imported to detect spent outputs [3].
  5. Subaddresses that belong to the same wallet can be linked via the Janus attack [4].
  6. The detection of outputs received to subaddresses is based on a lookup table, which can sometimes cause the wallet to miss outputs [5].

1.2 Jamtis

Jamtis is a next-generation addressing scheme that was developed specifically to tackle all of the shortcomings of CryptoNote addresses that were mentioned above. Jamtis comes with a new transaction protocol that is backwards compatible with existing CryptoNote addresses. That means wallets will be able to send to both new and old addresses and the resulting transactions will be indistinguishable in the blockchain.

Additionally, Jamtis comes with a new 16-word mnemonic scheme called Polyseed [6] that will replace the legacy 25-word seed for new wallets.

2. Features

2.1 Address format

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

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 "FilterAssist" is intended for wallet-scanning and only has the ability to calculate view tags [7].

View tags can be used to eliminate 99.6% of outputs that don't belong to the wallet. Possible use cases are:

2.2.1 Wallet component

A wallet can have a "FilterAssist" 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 "FilterAssist" 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 about 100. 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 Miscellaneous definitions

  1. The function BytesToInt256(x) deserializes a 256-bit little-endian integer from a 32-byte input.
  2. The function BytesToInt512(x) deserializes a 512-bit little-endian integer from a 64-byte input.
  3. The function RandBytes(x) generates a random x-byte string.
  4. Concatenation is denoted by ||.

3.2 Hash function

The function Hb(x) with parameters b, x, refers to the Blake2b hash function [8] 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.
  • The input x is hashed.

The function SecretDerive is defined as:

SecretDerive(x) = H32(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 [9].

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 constructed using the following KeyClamp1(i) function from a uniformly distributed 32-byte integer i:

  1. i[31] &= 0x7f (clear the most significant bit)
  2. i[0] &= 0xf8 (clear the least significant 3 bits)
  3. return i

Non-deterministic keys can be generated using the KeyGen1() function:

  1. i = BytesToInt256(RandBytes(32))
  2. return KeyClamp1(i)

Deterministic keys are derived using the following KeyDerive1(x) function:

  1. i = BytesToInt256(H32(x))
  2. return KeyClamp1(i)

The KeyClamp1 function causes all Curve25519 private keys to be multiples of the cofactor 8, which ensures that all public keys are in the prime-order subgroup. The multiplicative inverse modulo is calculated as 1/d = 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 [10]. The following generators are used:

Point Derivation Serialized (hex)
G generator of 𝔾2 5866666666666666666666666666666666666666666666666666666666666666
H Hp1(G) 8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94
T Hp2("Monero generator T") d1e6c1e625757d40bee4eed4fa6ad6447c426693f29dfb1c2fbb4c41e1f6bfd3

Here Hp1 and Hp2 refer to two hash-to-point functions.

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

KeyDerive2(x) = BytesToInt512(H64(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.3.3 Public key conversion

We define two functions that can transform public keys between the two curves:

  1. ConvertPubkey1(D) takes a Curve25519 public key D and outputs the corresponding Ed25519 public key K with an even-valued x coordinate.
  2. ConvertPubkey2(K) takes an Ed25519 public key K and outputs the corresponding Curve25519 public key D.

Additionally, we define the function NormalizeX(K) that takes an Ed25519 point K and returns K if its x corrdinate is even or -K if its x coordinate is odd.

3.4 Block cipher

The function BlockEnc(s, x) refers to the application of the Twofish [11] 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 [12].

4. Wallets

4.1 Wallet parameters

Each Jamtis wallet consists of two main keys, a timestamp and a bit flag:

Field Type Description
kps private key prove-spend key
svb secret key view-balance secret
birthday timestamp date when the wallet was created
jamtis bit always set to 1

The prove-spend key kps is required to spend money in the wallet and the view-balance secret svb 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.

Wallets with the jamtis bit set to 0 are legacy wallets using the CryptoNote address format and the legacy keys. Jamtis wallets always set the jamtis bit-flag to 1 and use the new address format.

4.2 Wallet creation

4.2.1 Standard wallets

Standard Jamtis wallets are generated as a 16-word Polyseed mnemonic [6], which provides the wallet master secret sm and also encodes the date when the wallet was created and the jamtis bit-flag. The keys kps and svb are derived from the master secret.

Field Derivation
sm from Polyseed
kps kps = KeyDerive2("jamtis_prove_spend_key" || sm)
svb svb = SecretDerive("jamtis_view_balance_secret" || sm)
birthday from Polyseed
jamtis from Polyseed

4.2.2 Multisignature wallets

Multisignature wallets are generated in a setup ceremony, where all the signers collectively generate the prove-spend key kps and the view-balance secret svb.

Field Derivation
kps setup ceremony
svb setup ceremony
birthday setup ceremony
jamtis setup ceremony

4.3 Additional keys

There are additional keys derived from svb:

Key Name Derivation Used to
kgi generate-image key kgi = KeyDerive2("jamtis_generate_image_key" || svb) generate key images
svr view-received secret svr = SecretDerive("jamtis_view_received_secret" || svb) find and decode received e-notes
dur unlock-received key dur = KeyDerive1("jamtis_unlock_received_key" || svr) derive e-note shared secrets
dir identify-received key dir = KeyDerive1("jamtis_identify_received_key" || svr) derive e-note shared secrets
dfa filter-assist key dfa = KeyDerive1("jamtis_filter_assist_key" || svr) calculate primary view tags
sga generate-address secret sga = SecretDerive("jamtis_generate_address_secret" || svr) generate addresses
sct cipher-tag secret sct = SecretDerive("jamtis_cipher_tag_secret" || sga) encrypt/decrypt address tags

The key kgi is required to generate key images.

The secret svr (and its child keys) provides the ability to calculate the sender-receiver shared secrets when scanning for received payments. The key dfa can recognize candidates for owned e-notes by matching the primary view tag.

The secret sga (and its child secret sct) is used to generate public addresses.

4.4 Key hierarchy

The following figure shows the overall hierarchy of wallet keys. Note that the master secret sm doesn't exist for multisignature wallets.

s_m (master secret)
 |
 |
 |
 +- k_ps (prove-spend key)
 |
 |
 |
 +- s_vb (view-balance secret)
     |
     |
     |
     +- k_gi (generate-image key)
     |
     |
     |
     +- s_vr (view-received secret)
         |
         |
         |
         +- d_ur (unlock-received key)
         |
         |
         |
         +- d_ir (identify-received key)
         |
         |
         |
         +- d_fa (filter-assist key)
         |
         |
         |
         +- s_ga (generate-address secret)
             |
             |
             |
             +- s_ct (cipher-tag secret)

4.5 Wallet public keys

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

Key Name Value
Ks spend key Ks = kgi G + kps T
Dur unlock-received key Dur = dur B
Dfa filter-assist key Dfa = dfa Dur
Dir identify-received key Dir = dir Dur

4.6 Wallet access tiers

The private key hierarchy enables the following useful wallet tiers:

Tier Secret Public keys Off-chain capabilities On-chain capabilities
AddrGen sga Ks, Dur, Dir, Dfa generate public addresses none
FilterAssist dfa - recognize all public wallet addresses eliminate the majority of non-owned outputs
ViewReceived svr Ks all view received (except of internal e-notes)
ViewAll svb Ks all view all
Master sm - 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 (FilterAssist)

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. This reduces the amount of data that a light wallet has to download by a factor of about 100.

4.6.3 Payment validator (ViewReceived)

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

4.6.4 View-only 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.

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

Each Jamtis address encodes the tuple (j', K1j, D2j, D3j, D4j), where j' is the encrypted value of j and the other four values are public keys.

5.1.1 Address keys

The four public keys are constructed as:

  • K1j = Ks + kgj G + ktj T
  • D2j = (1 / daj) Dur
  • D3j = (1 / daj) Dfa
  • D4j = (1 / daj) Dir

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

Keys Name Derivation
sgenj address index generators sgenj = SecretDerive("jamtis_address_index_generator" || sga || j)
kgj spend key extensions kgj = KeyDerive2("jamtis_spendkey_extension_g" || sgenj || Ks || j)
ktj spend key extensions ktj = KeyDerive2("jamtis_spendkey_extension_t" || sgenj || Ks || j)
daj address keys daj = KeyDerive1("jamtis_address_privkey" || sgenj || Dur || Dfa || Dir || j)

The address index generator sgenj can be used to prove that the address was constructed from the index j and the public keys Ks, Dur, Dfa, Dir.

5.1.2 Address tag

Each address additionally includes a 16-byte tag j' = BlockEnc(sct, j).

5.2 Address encoding

5.2.1 Address structure

An address has the following overall structure:

Field Size (bits) Description
Header 30* human-readable address header (§ 5.2.2)
K1 256 address key 1
j' 128 address tag
padding 1 a zero bit
D2 255 address key 2
D3 255 address key 3
D4 255 address key 4
Checksum 40* (§ 5.2.3)

* The header and the checksum are already in base32 format

5.2.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 [14], so legacy Monero software can never accidentally decode a Jamtis address.

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

The following 3 network types are defined:

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

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

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

5.2.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 is the binary payload and the checksum is the 8-character checksum (already in base32).

The binary payload encodes the address tuple (K1, j', D2, D3, D4), with a single padding bit inserted between j' and D2 to align all public keys to a character boundary. The total size of data is 1150 bits.

The total length of the encoded address is 244 characters (=6+230+8).

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

5.2.6 Recipient authentication

TODO

6. Transaction protocol

6.1 Transaction global fields

6.1.1 Unlock time

The unlock_time field is removed [15].

6.1.2 Payment ID

A single 8-byte encrypted payment ID field is retained for 2-output non-coinbase transactions for backwards compability with legacy integrated addresses. When not sending to a legacy integrated address, pid is set to zero.

The payment ID pid is encrypted by exclusive or (XOR) with an encryption mask mpid. The encryption mask is derived from the shared secrets of the payment e-note.

6.1.3 View tag size specifier

A new 1-byte field tag_size is added to specify the primary view tag size in bits. The permitted range of values is 1-16 (validated by a consensus rule), but a value of 8 is enforced by a relay rule.

6.1.4 Ephemeral public keys

Every 2-output transaction has one ephemeral public key De. Transactions with N > 2 outputs have N ephemeral public keys (one for each output). Coinbase transactions always have one key per output.

6.2 E-note format

Each e-note represents an amount a sent to a Jamtis address (j', K1, D2, D3, D4) or a legacy address (K1, K2).

An e-note contains the output public key Ko, the 2-byte combined view tag vt, the amount commitment Ca and the encrypted values of j' and a. For coinbase transactions, the amount commitment Ca is omitted and the amount is not encrypted.

6.2.1 The output key

The output key is constructed as Ko = K1 + kgo G + kto T, where kgo and kto are key extensions of the address spend key K1.

6.2.2 View tags

The 16-bit combined view tag consists of the primary view tag vt1 and the secondary view tag vt2. The primary view tag size can be in the range of 1-16 bits and is equal to the value of the transaction field tag_size. The remaining 16-tag_size bits form the secondary view tag. Each view tag is derived from a different shared secret.

In the case of hidden e-notes (§ 6.6.2), all 16 bits are used for vt2. The same applies to e-notes sent to legacy (CryptoNote) addresses.

6.2.3 Amount commitment

The amount commitment is constructed as Ca = ka G + a H, where ka is the commitment mask and a is the amount. Coinbase transactions have implicitly Ca = a H.

6.2.4 Address tag

The address tag j' is encrypted by exclusive or (XOR) with an encryption mask mj'.

6.2.5 Amount

The amount a is encrypted by exclusive or (XOR) with an encryption mask ma.

6.3 E-note types

There are 2 e-notes types: payment and change.

6.3.1 Payment e-notes

These e-notes represent a received payment and show up in the wallet transaction history as a positive amount. These e-notes can be received both from the outside or from the wallet itself (internal payments).

6.3.2 Change e-notes

These e-notes represent change returned back to the wallet after a payment is made. The UX difference is that the wallet will not display change e-notes in the transaction history as a positive amount, but rather will use the change to reduce the amount that was spent. Change e-notes always come from the wallet itself. If a change e-note has a zero amount, it's called a dummy e-note.

6.4 E-note derivations

The e-note components are derived from 3 shared secret keys X1, X2 and X4. The definitions of these keys are described below.

Component Name Derivation
vt1 primary view tag vt1 = SecretDerive("jamtis_primary_view_tag" || X1 || Ko)
vt2 secondary view tag vt2 = SecretDerive("jamtis_secondary_view_tag" || X2 || Ko)
mj' encryption mask for j' mj' = SecretDerive("jamtis_encryption_mask_j'" || X1 || X2 || Ko)
ma encryption mask for a ma = SecretDerive("jamtis_encryption_mask_a" || X4 || Ko)
mpid encryption mask for pid mpid = SecretDerive("jamtis_encryption_mask_pid" || X4 || Ko)
ka amount commitment mask ka = KeyDerive1("jamtis_commitment_mask" || X4 || enote_type)
kgo output key extension G kgo = KeyDerive1("jamtis_key_extension_g" || X4 || Ca)
kto output key extension T kto = KeyDerive1("jamtis_key_extension_t" || X4 || Ca)

The variable enote_type is "payment" or "change" depending on the e-note type.

6.5 Sender-receiver shared secrets

When sending to a Jamtis address (j', K1, D2, D3, D4), the sender first generates the sending private key de = KeyGen1() and includes De = de D2 in the transaction data.

The sender and the recipient can then both derive the following three shared keys:

Shared key Sender Recipient
X1 de D3 dfa De
X2 de D4 dir De
X3 de B (daj / dur) De

The fourth shared key is derived as follows:

Shared key Derivation
X4 X4 = SecretDerive("jamtis_shared_key" || X1 || X2 || X3 || De || input_context)

Here input_context is defined as:

transaction type input_context
coinbase block height
non-coinbase sorted list of spent key images

The purpose of input_context is to make X4 unique for every transaction.

6.5.1 Janus outputs

In case of a Janus attack, the recipient will derive different values of the shared keys X3 and X4 and will not recognize the output. The attacker will not be able to derive the recipient's value of X3 even if they know the value of daj for one of the involved addresses.

6.6 Internal e-notes

E-notes which go to an address that belongs to the sending wallet are called "internal e-notes". The most common type are change e-notes, but internal payments are also possible.

For internal e-notes, a different construction of the first three shared keys is used:

Shared key Value
X1 dfa De
X2 svb
X3 svb

This ensures that:

  1. Wallet tiers below ViewAll cannot recognize internal e-notes.
  2. For typical 2-output transactions, the change e-note can reuse the same value of De as the payment e-note.

Coinbase transactions are not considered to be internal.

6.6.1 Mandatory change

Every transaction that spends funds from the wallet must produce at least one internal e-note, typically a change e-note. If there is no change left, a dummy e-note is added (change with a zero amount). This ensures that all transactions relevant to the wallet have a matching primary view tag on at least one output.

6.6.2 Hidden e-notes

If a transaction produces more than one internal e-note (e.g. a payment e-note and a change e-note or two change e-notes), only one of them gets a primary view tag. For the remaining internal e-notes, all 16 bits are filled with the secondary view tag. This prevents the FilterAssist wallet tier from linking transactions to the wallet based on the number of primary view tag matches within a transaction.

The consequence of this rule is that a wallet scanning for incoming transactions has to scan all e-notes of transactions with at least one matching primary view tag. However, most of them will only have to be tested for a secondary view tag match (false positive rate of 1/65536).

6.7 Legacy addresses

When sending the amount a to a legacy address (K1, K2, pid), the sender will first generate a sending secret n = RandBytes(16) and derive de = KeyDerive1("jamtis_legacy_sending_key" || n || a || K1 || K2 || pid). The payment ID pid is considered to be zero for main addresses and subaddresses.

The e-note public key is defined as De = de ConvertPubKey2(K1) when sending to a subaddress and De = de B when sending to a main address or an integrated address.

The three shared keys X1, X2 and X3 are all equal and calculated as follows:

Shared key Sender Recipient
X1,2,3 ConvertPubKey1(de ConvertPubKey2(8 K2)) NormalizeX(8 kv ConvertPubKey1(De))

The e-note will only include the secondary view tag vt2 with a size of 16 bits. The value of j' to encrypt is set to the sending secret n.

6.7.1 Janus outputs

The protocol provides Janus mitigation for all legacy addresses. When receiving a payment, the recipient can decrypt n, rederive de and check if the e-note public key De was constructed correctly.

6.7.2 Scanning performance

When scanning for received e-notes, legacy wallets need to calculate NormalizeX(8 kv ConvertPubKey1(De)). The operation ConvertPubKey1(De) can be done during point decompression for free. The NormalizeX() function simply drops the x coordinate. The scanning performance for legacy wallets is therefore the same as in the old protocol.

Note: Legacy wallets use scalar multiplication in 𝔾2 because the legacy view key kv might be larger than 2252, which is not supported in the Montgomery ladder.

7. Test vectors

TODO

Credits

Special thanks to everyone who commented and provided feedback on the original Jamtis gist. Some of the ideas were incorporated in this document.

References

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

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 = (
    "xmra1mm95tp74ihjcu244xt4hpw1smcg5cdhdubfbmk3iyyw16ned1tu70h"
    "ys0r3784af7r8515f2p9xtrx58akjtwb0cft00ari8jecrighji8aqaexws"
    "h2475q3e1bay734kuhicey8bck5wwfbbp2yi4e4qn9h8dst5aaq8qnbyj0x"
    "rweamt1jwq5m0j1anh5srpm6fkhm6s76s3udi6xi0rm0jwf884j2exgg3t0")

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

Appendix B: Forward secrecy

Forward secrecy refers to the preservation of privacy properties of past transactions against a future adversary capable of solving the elliptic curve discrete logarithm problem (ECDLP), for example a quantum computer.

B.1 Legacy wallets

All e-notes sent to legacy addresses under this protocol are forward-secret unless an address that belongs to the legacy wallet is publicly known.

If an address is known to the ECDLP solver, all privacy is lost because the private view key kv can be extracted from the address to recognize all incoming e-notes. Once incoming e-notes are identified, the ECDLP solver will be able to learn the associated key images by extracting kga = DLog(K1, G) and calculating KI = (kga + kgo) Hp(Ko).

B.2 Jamtis wallets

Jamtis wallets offer better forward secrecy than legacy wallets.

All e-notes are forward secret unless an address that belongs to the Jamtis wallet is publicly known.

If a wallet address is known, the ECDLP solver will be able to extract the private keys dfa, dir and daj / dur.

  1. The knowledge of dfa will allow the adversary to slightly reduce the privacy of internal e-notes, similarly to using a Filter Assist 3rd party service.

  2. The knowledge of dfa and dir will allow the adversary to calculate view tags and nominal address tags for all external payments received to the wallet. There will be a certain number of false-positive matches, but incoming payments to any address that was used at least twice can be detected by finding repeated address tags.

  3. The knowledge of daj / dur will allow the adversary to derive the shared secret key X4, which will reveal all external payments to that specific address, including the amounts. If at least two such e-notes are spent, the adversary will also be able to detect outgoing payments from the address. This would require the calculation of the discrete logarithm of all key images with respect to the key image bases of the received payments. The adversary will be able to see repeated values of kgi + kgj, recognizing the spends.

B.3 Summary

ECDLP solver knows Legacy wallet Jamtis wallet
just blockchain data private1 private
one public address complete privacy loss privacy loss for external e-notes to that address
medium privacy reduction for other external e-notes
small privacy reduction for internal e-notes
all public addresses2 - privacy loss for all external e-notes
small privacy reduction for internal e-notes
  1. Except of e-notes received under the old transaction protocol
  2. Access to the GenAddr wallet tier
@jeffro256
Copy link

In Section 6.1.4, why do coinbase transactions all have one ephemeral pubkey per output?

@jeffro256
Copy link

Section 6.4: s/descrived/described

@jeffro256
Copy link

In section 6.6.2 "Hidden Enotes":

This prevents the FilterReceived wallet tier from probabilistically linking transactions to the wallet.

This is not totally true, the filter-assist (FilterReceived) tier can still probabilistically link transactions to the wallet if they have data from more than one transaction. The hidden enotes feature only prevents probabilistically determining owned enotes for a single transaction in isolation.

For the remaining e-notes, all 16 bits are filled with the secondary view tag.

This is problematic as the PaymentValidator tier can hone into which enotes are your self-sends with a greater degree of accuracy than it should [probability 2^(-16) versus 2^(-tag_size)]. In Implementing Seraphis, there's a completely different derivation for "auxiliary" enotes (I think I like the term "hidden" better actually), which is bound to the view-balance key, so completely opaque to the filter-assist or view-received key. This fixes that problem, but does add a little bit of complexity. Additionally, if we do this view-balance key based derivation, we can expand the size of the complementary view tag (if benchmarks and testing deemed it beneficial for scanning perf) without exacerbating this privacy issue.

@tevador
Copy link
Author

tevador commented May 1, 2024

Thank you for the review.

"filter-assist" instead of "filter-received"

OK, filter-assist sounds better.

The private address key daj should bind to the public spend key Ks, the address index j, as well as an 'address index generator' secret sgenj. This is because this allows index generator proofs to expose sgenj instead of sga. See Appendix A.2 of Implementing Seraphis.

All good ideas. Will add.

For Jamtis address public keys Dxj, we multiplied by daj instead of 1 / (daj) in the implementation so that we could use the crypto::x25519_invmul_key function on the balance recovery side to multiply those two scalars at once. Small detail though, won't kill performance either way.

I figured using 1 / d in the address is more efficient because you don't need to invert the scalar everytime you receive a payment. You only need to invert once when generating the address.

In Section 6.1.4, why do coinbase transactions all have one ephemeral pubkey per output?

Applying the general rule "one common pubkey for 2-output transactions" is not possible for coinbase transactions because it might be a 3rd party generating the transaction (e.g. p2pool) and they would be unable to generate the shared secrets for both outputs with just one common value of De. The rule for 2-output transactions only works if one of those e-notes is change.

This is not totally true,

OK, I will reword that to be more precise.

This is problematic as the PaymentValidator tier can hone into which enotes are your self-sends with a greater degree of accuracy than it should

The secondary view tag for self-spends (hidden e-notes are always self-spends) is:

vt2 = SecretDerive("jamtis_secondary_view_tag" || kvb || Ko)

(see tables in 6.4 and 6.6)

PaymentValidator tier doesn't know kvb, so it cannot derive the secondary tag for hidden e-notes. They will look like any unrelated e-note to them.

@jeffro256
Copy link

he secondary view tag for self-spends (hidden e-notes are always self-spends) is:

vt2 = SecretDerive("jamtis_secondary_view_tag" || kvb || Ko)

Okay sorry I must have skipped over that, that's a good path.

@UkoeHB
Copy link

UkoeHB commented May 2, 2024

How do you sign/prove a Cryptonote key image using the Jamtis-RCT spend key?

@tevador
Copy link
Author

tevador commented May 2, 2024

The spend authorization proof is described here in chapter 3.2: https://github.com/kayabaNerve/fcmp-ringct/blob/develop/fcmp%2B%2B.pdf

@UkoeHB
Copy link

UkoeHB commented May 2, 2024

Thank you

re-define output keys as O = x · G + y · T (where y = 0 for all existing outputs), and preserve the linking tag definition of x · HashToPoint(O).

If KI = k_vb Hp(K), then any view-balance wallet can burn enotes owned by the master wallet. See https://raw.githubusercontent.com/UkoeHB/Seraphis/master/implementing_seraphis/Impl-Seraphis-0-0-3.pdf Section 8.4.4 footnote 24 for a related issue.

@kayabaNerve
Copy link

kayabaNerve commented May 2, 2024 via email

@UkoeHB
Copy link

UkoeHB commented May 2, 2024

Ah right, interesting dynamic. I will think some more about it to be confident.

@jeffro256
Copy link

Optional Jamtis Feature Proposal:

Single Signer Self Sends Are Always Hidden, But it Requires a Onetime Scan with full Keys to Initiate (SSSSAAH-BROSKI)

Summary

This optional feature would be enabled by setting a flag in Polyseed, defaulting to false. This means that the user would have to commit to this feature being enabled/disabled for the lifetime of their wallet. Your self send enotes would change just as described: every one would be set to hidden (all view tag bits taken up by vt2). This would mean that your light wallet server learns absolutely no on-chain information about your self send enotes/transactions, not even probabilistic information (timing analysis still possible depending on the implementation). Your light wallet server will not send you scanning information about your self-send transactions, and as such, you are expected to keep track of these through other channels. Your light wallet server would only really be there to help you filter incoming received enotes from other parties. I believe this method of hiding all self-send enotes tackles the two biggest remaining on-chain data privacy attacks against LW users.

Pros

  • Defense against "LW2LW relationship" privacy attack (described below)
  • Defense against "Chosen LW sender transactions" privacy attack (also described below)
  • LWSs have less information about client's self sends in general
  • If you tell your LWS that you are a SSSSAAH-BROSKI wallet, you can reduce the number of enotes you need to download/scan against by about 2x since you don't need to download potential hidden enotes, only those enotes with matching primary view tags.
  • Indistinguishable on-chain from normal Jamtis/Legacy

Cons

  • In the case of a missing or corrupted wallet cache, the LW user must perform a full wallet scan up to the present chainstate before switching back to remote filter-assist scanning
  • Using a LW in a multisig setup becomes much trickier because you must know the txids of all signed self-send transactions, whether or not you signed them
  • This scheme is not friendly to using multiple separate LW wallet caches at once under certain implementations.
  • If you want to learn contextual information about your self sends txs from your LWS, without giving away which ones are those, your LWS needs to send you a list of all txids that are entering the chain in which block.
  • Telling your LWS that you are using a wallet with SSSSAAH-BROSKI enabled gives away persistent info.

Privacy attacks

LW2LW Relationship

If a LWS has the filter-assist keys of two users who send XMR between themselves (doesn't have to be two-way), then they can see that these two users are both involved in an overlapping set of transactions where the primary view matches are complementary. This is because one primary view tag will match for the recipient, and one for the sender. The more transactions there are between them, the exponentially more likely it is that these two users are sending funds between each other from the PoV of the LWS. The LWS can extend this tracking technique to multiple users to build up a probabilistic social graph.

SSSSAAH-BROSKI thwarts this attack by severing the link on the sender side, so that only the receiver gets their primary view tag matched. From the PoV of the LWS, even if they have both filter-assist keys, they get no information about the sender's view tags, and thus cannot probabilistically find overlapping transactions sets and thus cannot infer transaction relationships from on-chain data.

Chosen LW Sender Transactions

This attack involves the attacker having a preselected set of N transactions, and then searching for a sender of some of those (or all) transactions. This attacker may obtain this list of transactions from, say, raiding your favorite Kinder egg distributor. They can then corroborate or compromise your LWS (or be the same entity), and send them the set of N transactions. The expected number of transactions that should contain a primary view tag match for any random filter-assist key is roughly N/(2^(tag_size)), not factoring in the number of outputs per transaction (the point is that it's a small fraction). And more importantly, its exponentially less likely that a random user will have a matching primary view tag for a increasing long sequence of non-associated transactions. So the attacker/LWS could search for a user with an abnormally high number of matching transactions from that set and link that user to those transactions with a high degree of certainty.

Again, SSSSAAH-BROSKI thwarts this attack by severing the link on the sender side. This does not do anything to mitigate this attack on the receiving side, but it does basically eliminate it on the sender side.

@tevador
Copy link
Author

tevador commented May 2, 2024

If KI = k_vb Hp(K), then any view-balance wallet can burn enotes owned by the master wallet.

Spending an output with the key K = x G + y T requires the knowledge of both x and y (otherwise you can't make a valid composition proof that's part of the SA+L proof). If you use a different value of K, the key image will be different. So AFAICS, no, the view-only wallet cannot burn the e-note. The only thing it can do is calculate the key image to detect when the e-note is spent.

@jeffro256
Copy link

@UkoeHB I made that same mistake too since I was in the Seraphis thinking mode where we can malleate the onetime-address and still get the same key image generator.

@tevador
Copy link
Author

tevador commented May 2, 2024

This optional feature would be enabled by setting a flag in Polyseed, defaulting to false.

I haven't fully analyzed the proposal, but I personally don't think this feature is important enough to spend a Polyseed bit on (there are only 2 unallocated bits left). The attacks you describe only apply to users who give up their filter-assist key, which is an explicit opt-out of the strongest privacy guarantees of the protocol.

@jeffro256
Copy link

The NormalizeX() function simply drops the x coordinate.

Shouldn't it be "drops the y coordinate"?

@tevador
Copy link
Author

tevador commented May 3, 2024

Ed25519 points are serialized as the y-coordinate and the parity of the x-coordinate. NormalizeX() normalizes the parity of x to be even (by conditionally negating the point). This has technically the same effect as y-only serialization, i.e. dropping the x-coordinate. So there is no actual computation involved.

@jeffro256
Copy link

This allows the address itself to be transferred via an insecure channel.

It may be worth nothing that the 125 bit output may be enough to prevent preimage attacks with standard strength, but not collision attacks. We should also maybe specify that whichever hashing algorithm we use to calculate the RIDs, it needs to be slow in order to make finding preimages harder.

@tevador
Copy link
Author

tevador commented May 6, 2024

but not collision attacks

Collision attacks on RIDs are not an issue. In order to find a collision, the attacker would need to generate both addresses that collide (this is how collision attacks work). So they can only replace between a pair of addresses they both own, which is not useful for any practical attack.

If the attacker wanted to swap a 3rd party address for their own with the same RID, that's not a collision attack anymore, but a preimage attack, which is infeasible at 120 bits.

The original implementation of RIDs is in this repository, although dashes have been since replaced with underscores to enable double-click selection and also we use a different base32 variant now.

@jeffro256
Copy link

It could be an issue if a service misused RIDs as unique identifiers. For example, let's say there was a service that let's you withdraw XMR and it provides you a receipt that contains an RID. An attacker could switch out the address for another one with the same RID and claim that the receipt wasn't valid since they can't produce a valid payment proof for that address.

That's why I think we should note its lack of collision resistance and that they should NOT be used as identifiers. You should really only rely on RIDs from trusted sources (AKA from people who won't perform collision attacks). As such, the term "RID" might be inappropriate for what this human-readable preimage resistant string is.

@jeffro256
Copy link

@kayabaNerve and I discussed Janus mitigations for main addresses yesterday and came to the conclusion that we'd like to see space specifically dedicated to the payment ID, so that main addresses can be guaranteed Janus protections (by the same method of encoding randomness then recomputing D_e). We disagree on whether or not to make it per-output or per-tx, but I'd like to be able to guarantee to the users that their main addresses are Janus protected unconditionally.

I say that we provide the payment ID space per-tx since that's how its currently done and takes less space. We could then phase it out in X months now that we have a replacement with Jamtis. @kayabaNerve proposes to make the payment ID space per-output so that we can send to multiple integrated addresses within one transaction.

@tevador
Copy link
Author

tevador commented May 7, 2024

An attacker could switch out the address for another one with the same RID and claim that the receipt wasn't valid since they can't produce a valid payment proof for that address.

The sender can provide the original address with the payment proof and show that it has the same RID. This proves that the attacker is to blame.

we should note its lack of collision resistance

I'm planning to mention this in section 5.2.6.

As such, the term "RID" might be inappropriate for what this human-readable preimage resistant string is.

On the contrary, I think the term exactly describes what it is. It identifies the recipient, who generated the (one or more) addresses that match this RID. Basically, if you get an address that matches an RID, you can be pretty sure it comes from the person who created the RID.

You should really only rely on RIDs from trusted sources (AKA from people who won't perform collision attacks)

I'm not aware of any useful attacks that can be done with collided RIDs. The sender can always prove that they are the victim of a collision attack, which only the recipient could have performed.

Actually, bitcoin addresses are 160-bit hashes, so they are also susceptible to collision attacks (with a work factor of 280), but as with RIDs, I'm not aware of any useful attacks.

To be on the safe side, we could increase RIDs to 160 bits to match bitcoin. This would need 32 characters if we lose the checksum.

@jeffro256
Copy link

To be on the safe side, we could increase RIDs to 160 bits to match bitcoin. This would need 32 characters if we lose the checksum.

I think we should leave them the size that they are since preimage resistance is definitely more important than collision resistance, and making them longer might hinder the main use case. Just as long as we communicate that RIDs from untrusted sources shouldn't be used to uniquely identify addresses from that same untrusted source then we should be fine.

@tevador
Copy link
Author

tevador commented May 7, 2024

we'd like to see space specifically dedicated to the payment ID, so that main addresses can be guaranteed Janus protections

I'm not opposed to this. The current solution is the best you can do for legacy addresses without increasing the tx size beyond what the Jamtis protocol needs.

I say that we provide the payment ID space per-tx since that's how its currently done and takes less space. We could then phase it out in X months now that we have a replacement with Jamtis.

I prefer this solution.

@kayabaNerve
Copy link

kayabaNerve commented May 7, 2024 via email

@tevador
Copy link
Author

tevador commented May 7, 2024

We can keep the single legacy PID for the time being. Merchants who want to batch payments to integrated addresses should migrate to Jamtis instead, which provides basically the same function (even expanding the PID from 8 to 16 bytes).

@kayabaNerve
Copy link

kayabaNerve commented May 7, 2024 via email

@jeffro256
Copy link

I remember why we used 2 different shared secret derivations for selfsend enotes: we need a way to derive different enote components in the case of a 2-output tx where both are selfsends since they use 1 ephemeral pubkey. This proposal in its current form does not have it and thus cannot have 2 output selfsends without burning funds.

@jeffro256
Copy link

Or we could go back to the somewhat ugly method of binding shared secrets to the output's index within the transaction.

@tevador
Copy link
Author

tevador commented May 7, 2024

I remember why we used 2 different shared secret derivations for selfsend enotes: we need a way to derive different enote components in the case of a 2-output tx where both are selfsends since they use 1 ephemeral pubkey. This proposal in its current form does not have it and thus cannot have 2 output selfsends without burning funds.

If you mean a 2-output transaction where both outputs are "payment" type e-notes, then this is forbidden. Either you have a 2-output tx with one "payment" e-note and one "change" e-note or you need a 3-output transaction.

If you were making 2 batched payments to 2 different people, you would also need a 3-output transaction, so I don't think this is a problem. "Batched self-payments" are quite a niche use-case anyways.

@jeffro256
Copy link

jeffro256 commented May 7, 2024

Either you have a 2-output tx with one "payment" e-note and one "change" e-note or you need a 3-output transaction.

My apologies, I didn't realize the chain of dependencies enote_type <- ka <- C <- Ko, Ko <- ma, and Ko <- mj' causes Ko, ma, and mj' to all be unique for a different enote_type without trying 2 different intermediate self send secrets (not including recomputing Ca). That's an improvement over the current Jamtis implementation, nice!

Another observation: depending on the speed of (scalar scalar) multiplication vs (scalar point) multiplication) as well as the total size of the view tag, it might be worth binding the output key extensions to X2 (with input_context and D_e) instead of X4, in order to avoid multiplying by inverse dja while recomputing Ko.

@jeffro256
Copy link

In Section 2.2.1:

A wallet can have a "FilterAssist" 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).

The 256 figure is not quite accurate since the filter-assist server must send all enotes in a transaction with at least one matching primary view tag, since they are potential hidden enotes. We do not need to do any Diffie-Helman exchanges or scalar multiplications before matching view tags on hidden enotes though, so computationally they are ~100x faster than normal enotes, but we do have to download them which multiplies our bandwidth usage by the average number of outputs per transaction. So it's more like "the wallet can thus be synchronized at least (2^tag_size) / average_num_outs_per_tx faster..."

@tevador
Copy link
Author

tevador commented May 12, 2024

See 2.2.2:

If the "FilterAssist" 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 about 100.

Section 2.2.1 speaks about a local wallet component that doesn't need to send anything over the network. It's simply waiting for the user to enter the password to "open" the wallet.

@tevador
Copy link
Author

tevador commented May 13, 2024

FYI, I made a change in the key hierarchy. The generate-address secret s_ga is now derived directly from k_vb instead of from d_vr. The reason for this change is to provide slightly stronger forward secrecy in case a public address is known to a DLP-solving adversary.

Previously, the adversary would be able to extract d_vr from the address (by solving the DLP between D_2 and D_4), derive s_ga, remove any G-terms from the address and learn k_vb. They would thus be able to detect all spends.

With the new derivation, the adversary won't be able to detect any spends from the wallet even if they have an address that belongs to the wallet.

@tevador
Copy link
Author

tevador commented May 13, 2024

The generate-address secret s_ga is now derived directly from k_vb instead of from d_vr.

Unfortunately, this has a side-effect of creating an unintentional wallet tier that can only calculate view tags (both primary and secondary) and nominal address tags. This tier could be abused by 3rd party scanning services.

However, I still support this change because it makes Jamtis wallets more resistant to DLP-solving attackers. With legacy wallets, publishing a single address will allow the attacker to see all payments coming to the wallet. With Jamtis, the attacker will only see payments coming to that one address. That's because the attacker can't derive daj for addresses they don't know.

@jeffro256
Copy link

jeffro256 commented May 13, 2024

Previously, the adversary would be able to extract d_vr from the address (by solving the DLP between D_2 and D_4), derive s_ga, remove any G-terms from the address and learn k_vb. They would thus be able to detect all spends.

They cannot learn kvb since Ks perfectly hides kvb with the km factor.

@tevador
Copy link
Author

tevador commented May 13, 2024

They cannot learn k_vb since Ks perfectly hides kvb with the km factor.

You are correct that they cannot do it from the address alone, but they would be able to extract kvb with the help of blockchain data.

Recall that key images are calculated as: KI = (kvb + kgj + kgsender) Hp2(Ko).

All it takes is for you to receive and then spend two outputs. The adversary knows Ko, kgj and kgsender. If they calculate the discrete log of all key images with the bases of Hp2(Ko) and subtract the corresponding kgj and kgsender, they will eventually get the same number twice - your private key kvb.

@jeffro256
Copy link

Okay I see, great spot

@tevador
Copy link
Author

tevador commented May 13, 2024

With the new derivation, the adversary won't be able to detect any spends from the wallet even if they have an address that belongs to the wallet.

I have to correct myself here. The adversary will be able to detect some spends with the new derivation, but only for the addresses they know and only if at least two e-notes received to that address are spent. The process is the same as I described above, except the adversary can learn kvb + kgj.

This actually kind of removes the distinction between forward-secret addresses and forward-secret one-time keys that @kayabaNerve was talking about here.

@jeffro256
Copy link

The adversary will be able to detect some spends with the new derivation, but only for the addresses they know and only if at least two e-notes received to that address are spent.

For Jamtis addressing, there only needs to be a single spend of any enote to any public address, of which you don't need to know beforehand. And this attack can completely run start-to-finish in O(K*N) time, where K is the number of on-chain key images and N is the number of received plain enotes. Here how it would work:

If an attacker knows d_vr, he can derive s_ga, and then s_ct. They can also recover the base spendkey Ks from any single public address. Thus, for every single incoming enote, he knows Ko, j, kgj, and kgsender. For every single key image on-chain and every (kgj, kgsender, Hp2(Ko)) tuple (1 per received enote), the attacker would calculate kvb' = dlog(KI, Hp2(Ko)) - kgj - kgsender. The attacker would then calculate km' = dlog(Ks - kvb' G, T). Finally, the attacker checks kvb' ?= view_balance_key_derive(km'). If this test passes, then kvb' is the correct view-balance key, and we terminate.

@kayabaNerve
Copy link

My F-S addr/F-S OTK commentary is already nullified if the change output is traceable from a public facing address. While I'd love F-S OTKs (can find outputs to a F-S addr, cannot find when they're spent), changes being traceable with high probability would need to not be the case.

(I'm unsure where the current proposal is re: functionality, would like to again bring up PID preservation and Janus mitigations for main addresses, and furthermore, Carthage must be destroyed)

@tevador
Copy link
Author

tevador commented May 13, 2024

If an attacker knows d_vr, he can derive s_ga

Hence this doesn't apply anymore to the current Jamtis specs.

sga = SecretDerive("jamtis_generate_address_secret" || kvb)

I'm unsure where the current proposal is re: functionality, would like to again bring up PID preservation and Janus mitigations for main addresses

It's on my TODO list. It's not a one-line change in the specs.

@kayabaNerve
Copy link

Completely understand :) Thanks for the ACK and sorry to be the bother.

@jeffro256
Copy link

Hence this doesn't apply anymore to the current Jamtis specs.

Right. However, deriving sga from kvb instead of dvr still doesn't stop a DLP solver from solving for kvb if a DLP solver knows KI, kgj, and kgsender. A solver could have kgj and kgsender exposed to them in other ways (e.g. being given an address index proof and an enote ownership proof for a spent enote). See Appendix A.2 and A.3 of Implementing Seraphis for address index and enote ownership proofs, respectively.

What we could do is define a "spendkey image" key ksi = Hksi(kvb) and use this key instead of the view-balance key when constructing the base spendkey: Ks = ksi G + km T. This way, if a DLP solver gets a hold of a public address, thus dvr, and thus all kgsender, then they will only be able to correlate the key images to enotes where they know the address index extensions (which they can't compute themselves since sga isn't derived from dvr anymore), and not the entire account. Still not ideal, but this somewhat compartmentalizes the damage.

@tevador
Copy link
Author

tevador commented May 14, 2024

What we could do is define a "spendkey image" key ksi = Hksi(kvb) and use this key instead of the view-balance key when constructing the base spendkey: Ks = ksi G + km T

AFAICS, this doesn't help. If the DLP solver knows Ks (which you assume they do, e.g. from an address index proof), learning ksi means they can just calculate km = DLog(Ks - ksi G, T), which leaks everything about the wallet.

We would need something like this:

s_m (master secret)
 |
 |
 |
 +- k_ps (prove-spend key)
 |
 |
 |
 +- s_vb (view-balance secret)
     |
     |
     |
     +- k_gi (generate-image key)
     |
     |
     |
     +- s_vr (view-received secret)
         |
         |
         |
         +- d_ur (unlock-received key)
         |
         |
         |
         +- d_fa (filter-assist key)
         |
         |
         |
         +- s_ga (generate-address secret)
             |
             |
             |
             +- s_ct (cipher-tag secret)

The public spend key is calculated as K_s = k_gi G + k_ps T.

Unfortunately, the DLP solver still learns everything about the wallet if you ever give away your AddrGen wallet tier (for example, to a 3rd party service that can generate payment addresses on demand). I'm not sure if there is a way to fix that and make AddrGen safer to share.

Edit: slightly better key hierarchy for view-received.

@tevador
Copy link
Author

tevador commented May 15, 2024

I made two updates to the specs:

  1. Restored the encrypted payment ID field for 2-output transactions. This enables Janus attack mitigations for all legacy addresses.
  2. Improved the private key hierarchy. Jamtis wallets will now only lose forward privacy for payments associated with publicly known addresses.
    • The new hierachy never uses elliptic curve private keys to derive child keys. This prevents a DLP solver from learning any additional keys apart from those leaked by the discrete log.
    • The previous view-received key d_vr, which was used for two different DH exchanges, was split into two new keys d_ur (unlock-received key) and d_ir (identify-received key). Also the filter-assist key d_fa is now derived independently, so a DLP solver needs to solve 3 distinct discrete logs in order to learn the shared secrets associated with a public address.

@kayabaNerve
Copy link

🥳

@jeffro256
Copy link

jeffro256 commented May 16, 2024

Even with these different derivation schemes, a DLP solver with only one of your public addresses can correlate key images to the incoming enotes for all addresses where 2 or more enotes to that address were spent, as well as correlate the enotes made out to the same address. As we've already established, the DLP solver knows dvr and thus kgsender for all incoming enotes. For any two key images KI1 and KI2, the attacker can calculate address-generate-image components kagi,1 = dlog(KI1, Hp(Ko,1)) - kg,1sender and kagi,2 = dlog(KI2, Hp(Ko,2)) - kg,2sender. If kagi,1 = kagi,2, then KI1 is the key image for Ko,1, KI2 is the key image for Ko,2, and Ko,1 and Ko,2 were made out to the same public address. This can all be done without knowledge of sga or any address index extensions.

Edit: they can only do this for known public addresses as @tevador points out below

@tevador
Copy link
Author

tevador commented May 16, 2024

the DLP solver knows d_vr and thus k_g^sender for all incoming enotes

Not true. In order to get the key extensions, you need to know the shared key X3 = (daj / dur) De. The DLP solver will only know daj / dur for public addresses.

The DLP solver will however know the key dir, so they will be able to decrypt the nominal address tags even for addresses they don't know. This will leak incoming payments in case the same (unknown) address is used 2 or more times. However, spends from such addresses cannot be identified, as well as any secrets derived from the shared key X3 (e.g. the amount or any additional encrypted memos).

Also all self-spends are safe, even if they use a public address.

@tevador
Copy link
Author

tevador commented May 17, 2024

I added Appendix B, which talks about the forward secrecy properties.

The summary is in this table:

ECDLP solver knows Legacy wallet Jamtis wallet
just blockchain data private1 private
one public address complete privacy loss privacy loss for external e-notes to that address
medium privacy reduction for other external e-notes
small privacy reduction for internal e-notes
all public addresses2 - privacy loss for all external e-notes
small privacy reduction for internal e-notes
  1. Except of e-notes received under the old transaction protocol
  2. Access to the GenAddr wallet tier

I also renamed "self-spends/self-sends" to "internal e-notes" because I find that term clearer. The opposite are "external e-notes". There are also some additional small tweaks in the specs (e.g. renaming kgsender to kgo for readability).

@jeffro256
Copy link

jeffro256 commented May 27, 2024

Why does the private address key daj bind to the account pubkeys? The most notable here is Dur, which would reveal dur to a DLP solver in an address index proof where it wouldn't otherwise be revealed by knowing a public address.

@tevador
Copy link
Author

tevador commented May 27, 2024

Without binding to all public keys, how would you prove that an address is constructed correctly?

If proving just the spend key is enough, we can remove the public keys from the daj derivation, although the same can be achieved by simply not sharing Dur, Dfa, Dir.

@jeffro256
Copy link

There's a relevant issue here: UkoeHB/Seraphis#8. I'm not convinced binding is necessary actually, and might retract my comment here: https://gist.github.com/tevador/d3656a217c0177c160b9b6219d9ebb96?permalink_comment_id=5043111#gistcomment-5043111.

@jeffro256
Copy link

jeffro256 commented Jul 15, 2024

We need another Janus protection path for 2-ouput shared De transactions to legacy addresses, preferably one where we can mix legacy and Jamtis destinations together. There's the issue with rederiving de for Janus protection in 2-output transactions: we only have one de but need to recompute it with two different pairs of addresses (the external recipient and our change address). Doing that requires finding a hash collision. I suggest for change enotes, we send to a "secret" change subaddress spend pubkey. If this secret change subaddress spend pubkey is revealed, then people can perform Janus attacks on you. Similar to how subaddress spend pubkeys are generated Ksj = Ks + H_n(kv || j) G, we would generate the secret change subaddress spend pubkey as follows: Kschange = Ks + H_n(kv || Ks || "secret change subaddress spend pubkey") G (but also with T generator extensions). We skip the de recomputation check if and only if subtracting the onetime extensions from the onetime address returns our secret change subadress spend pubkey.

Edit: bind the change address extensions to Ks to prevent recovering of the primary address spend pubkey from the change address.

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