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

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;
}

@tevador
Copy link
Author

tevador commented Feb 1, 2022

fe25519_invert(one_minus_y, one_minus_y);

Field inversion is one of the slowest field operations. This is definitely not a cheap operation.

@UkoeHB
Copy link

UkoeHB commented Feb 1, 2022

Field inversion is one of the slowest field operations. This is definitely not a cheap operation.

EDIT: My point is the built-in function to convert ed25519->x25519 isn't super efficient since it fully deserializes the ed25519 point. A proper test needs this cheaper conversion function ^

@j-berman
Copy link

j-berman commented Feb 1, 2022

@tevador are you using an assembly implementation for x25519 also?

@UkoeHB
Copy link

UkoeHB commented Feb 1, 2022

@tevador The key r G baked into the amount commitment and encryption mask, I think it needs to be 8 r G.

@tevador
Copy link
Author

tevador commented Feb 1, 2022

@UkoeHB FYI, I benchmarked the crypto_sign_ed25519_pk_to_curve25519_remove_extra_ops function on my machine at ~13500 clock cycles, which is only slightly faster than full ed25519 point decompression taking ~15000 cycles.

@j-berman This is the x25519 code I'm benchmarking:

static void x25519_scalarmult(fe25519* xs, const uint8_t scalar[32], const fe25519* xb) {
    fe25519 zs, xt, zt, t0, t1;
    uint8_t swap = 0;

    fe25519_setint(xs, 1);
    fe25519_setint(&zs, 0);
    xt = *xb;
    fe25519_setint(&zt, 1);

    for (int pos = 254; pos >= 0; --pos) {
        uint8_t b = 1 & (scalar[pos / 8] >> (pos & 7));

        swap ^= b;
        fe25519_cswap(xs, &xt, swap);
        fe25519_cswap(&zs, &zt, swap);
        swap = b;
        fe25519_sub(&t0, &xt, &zt);
        fe25519_sub(&t1, xs, &zs);
        fe25519_add(xs, xs, &zs);
        fe25519_add(&zs, &xt, &zt);
        fe25519_mul(&zt, xs, &t0);
        fe25519_mul(&zs, &zs, &t1);
        fe25519_square(&t0, &t1);
        fe25519_square(&t1, xs);
        fe25519_add(&xt, &zt, &zs);
        fe25519_sub(&zs, &zt, &zs);
        fe25519_mul(xs, &t1, &t0);
        fe25519_sub(&t1, &t1, &t0);
        fe25519_square(&zs, &zs);
        fe25519_mul121666(&zt, &t1);
        fe25519_square(&xt, &xt);
        fe25519_add(&t0, &t0, &zt);
        fe25519_mul(&zt, xb, &zs);
        fe25519_mul(&zs, &t1, &t0);
    }

    fe25519_invert(&zs, &zs);
    fe25519_mul(xs, xs, &zs);
}

The functions fe25519_add, fe25519_sub, fe25519_mul, fe25519_square and fe25519_mul121666 are all written in assembly. Some small speed up should be possible by inlining everything.

@tevador
Copy link
Author

tevador commented Feb 2, 2022

Here are some thoughts on optimizing the view tag calculation. I'm writing it here rather than in the view tag PR since the decision to use keccak seems to be final there.

I benchmarked keccak at ~3200 cycles per hash on my machine. This means there is an additional ~2% speed up possible by replacing keccak with something faster.

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.

We don't actually need to hash the output key. It should be enough to hash the encrypted address tag t~, which is always different for change outputs.

Grin's PoW uses a Siphash-2-4 variant that accepts a 256-bit key and only hashes a 64-bit nonce. So we could modify the view tag calculation as: v = siphash24(t~, Kd) & 0xff using the shared secret as the 256-bit siphash key directly. I benchmarked that this takes just 34 cycles on my Ryzen 3700X. We could even use siphash-4-8, which takes 65 cycles.

Someone might argue against using a finite field element directly as a symmetric key due to its non-uniform distribution, but I think it's irrelevant in this case. The shared secret still contains about 254 bits of information: 1 bit is always zero because the size of the field is only 255 bits (this is for x25519) and another bit is removed because the field element plugged into an equation must produce a quadratic residue modulo 2255-19. A 254-bit secret key is plenty and siphash already mixes all key bits into the result, taking care of any non-uniformity.

Overall, v = siphash48(t~, Kd) & 0xff seems like a reasonably conservative approach that's still 50x faster than keccak.

This together with the move to x25519 for DH would result in a noticeable ~15% speed up.

Here is the full siphash48 source code I used for my bechmarks (you can get siphash24 by removing half of the siprounds):

#include <stdint.h>

#define ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b))))

#define U8TO64_LE(p)                                                           \
    (((uint64_t)((p)[0])) | ((uint64_t)((p)[1]) << 8) |                        \
     ((uint64_t)((p)[2]) << 16) | ((uint64_t)((p)[3]) << 24) |                 \
     ((uint64_t)((p)[4]) << 32) | ((uint64_t)((p)[5]) << 40) |                 \
     ((uint64_t)((p)[6]) << 48) | ((uint64_t)((p)[7]) << 56))

#define SIPROUND                                                               \
    do {                                                                       \
        v0 += v1;                                                              \
        v1 = ROTL(v1, 13);                                                     \
        v1 ^= v0;                                                              \
        v0 = ROTL(v0, 32);                                                     \
        v2 += v3;                                                              \
        v3 = ROTL(v3, 16);                                                     \
        v3 ^= v2;                                                              \
        v0 += v3;                                                              \
        v3 = ROTL(v3, 21);                                                     \
        v3 ^= v0;                                                              \
        v2 += v1;                                                              \
        v1 = ROTL(v1, 17);                                                     \
        v1 ^= v2;                                                              \
        v2 = ROTL(v2, 32);                                                     \
    } while (0)

uint64_t siphash48(const uint64_t in, const uint8_t key[32]) {
    uint64_t v0 = UINT64_C(0x736f6d6570736575);
    uint64_t v1 = UINT64_C(0x646f72616e646f6d);
    uint64_t v2 = UINT64_C(0x6c7967656e657261);
    uint64_t v3 = UINT64_C(0x7465646279746573);
    
    v3 ^= U8TO64_LE(key + 0);
    v2 ^= U8TO64_LE(key + 8);
    v1 ^= U8TO64_LE(key + 16);
    v0 ^= U8TO64_LE(key + 24);

    v3 ^= in;

    SIPROUND;
    SIPROUND;
    SIPROUND;
    SIPROUND;

    v0 ^= in;
    v2 ^= 0xff;

    SIPROUND;
    SIPROUND;
    SIPROUND;
    SIPROUND;
    SIPROUND;
    SIPROUND;
    SIPROUND;
    SIPROUND;

    return (v0 ^ v1) ^ (v2 ^ v3);
}

@UkoeHB
Copy link

UkoeHB commented Feb 2, 2022

We don't actually need to hash the output key. It should be enough to hash the encrypted address tag t~, which is always different for change outputs.

You're right that practically speaking enc_addr_tag could also work. I would feel a bit more confident tying the view tag to a core part of the output structure (instead of what is basically a memo field). Is there a meaningful disadvantage to hashing Ko?

I also think it needs a domain separator.

@tevador
Copy link
Author

tevador commented Feb 2, 2022

I would feel a bit more confident tying the view tag to a core part of the output structure (instead of what is basically a memo field).

The view tag itself is basically a memo field and its value is irrelevant to consensus. I don't think we gain anything by tying it to the output key. Or do you have some specific kind of attack in mind?

Is there a meaningful disadvantage to hashing Ko?

Mainly a performance reason as the output key is 4 times larger. I guess this is only relevant for siphash. Keccak has sufficiently large block size to accept more input without a performance impact.

I also think it needs a domain separator.

That's easy. We can use our own set of initial values for v0-v3 (the default is "somepseudorandomlygeneratedbytes").

@UkoeHB
Copy link

UkoeHB commented Feb 2, 2022

The view tag itself is basically a memo field and its value is irrelevant to consensus. I don't think we gain anything by tying it to the output key. Or do you have some specific kind of attack in mind?

Generally you make a hash dependency directly on things upstream of your object, not in a separate branch (address tags and view tags are separate). This ensures efficient coupling (the onetime address is the most 'stable' part of the output structure - least likely to change) and makes reasoning about the dependency structure more straightforward (no need to add an extra clause 'we chose this over this because of some tiny perf win'). In this case, you'd have to justify that the reduced cycle count from a smaller hash input is worth a non-standard coupling relationship (it will be < 0.2% perf difference I'm guessing).

@tevador
Copy link
Author

tevador commented Feb 2, 2022

Generally you make a hash dependency directly on things upstream of your object, not in a separate branch (address tags and view tags are separate)

Output keys, view tags and encrypted address tags are all "siblings" derived from the shared secret. I wouldn't say the output key is "upstream" of the view tag. The purpose of the view tag is to tell a potential recipient that the shared secret is probably correct.

the onetime address is the most 'stable' part of the output structure - least likely to change

I would argue that the address tags are equally unlikely to change since that would require a new addressing scheme. The most stable part of the output structure is the amount commitment, which is the only field that is common between RCT and Seraphis (and actually must stay the same forever otherwise you could never prove that transactions using old inputs balance out).

In this case, you'd have to justify that the reduced cycle count from a smaller hash input is worth a non-standard coupling relationship

Higher performance and simpler code would be two reasons to use the address tag rather the output key. AFAICS there are no reasons to use the output key or the amount commitment. The most straightforward structure would be to hash the shared secret and the output index (this is done in the current Monero PR). Since we don't want output indices, the next best thing is the address index. Seems straightforward to me.

@UkoeHB
Copy link

UkoeHB commented Feb 2, 2022

Output keys, view tags and encrypted address tags are all "siblings" derived from the shared secret. I wouldn't say the output key is "upstream" of the view tag. The purpose of the view tag is to tell a potential recipient that the shared secret is probably correct.

I don't think this is quite right. Both view tags and encrypted address tags are children of the onetime address.

  1. The view tag indicates if 'you might own the output' (ownership is 'have privkeys of the onetime address').
  2. Encrypted address tags tell you 'how to try to open the onetime address'.
  3. The shared secret is just an intermediate step toward opening up the onetime address, used to hide who has the right to open it from observers.

I would argue that the address tags are equally unlikely to change since that would require a new addressing scheme.

The core part of outputs remains the same between Seraphis and RingCT: a onetime address and an amount commitment. The internal structures of those points are opaque in this context.

If the RingCT view tag proposal were compatible with our needs here (which is theoretically possible [just do v = Siphash[16_bytes(K_d)]("xmr view tag", Ko), where K_d and K_o are opaque])- although practically speaking won't happen), then we wouldn't need to add any code for this. Tying view tags to the address index also ties them to the address scheme.

EDIT:

Higher performance and simpler code would be two reasons to use the address tag rather the output key.

Also, I think a custom implementation of Siphash is 'less simple' even if there are fewer LoC, because validating it is more difficult than checking if you copy pasted from a known implementation.

@tevador
Copy link
Author

tevador commented Feb 2, 2022

The output tuple (Ke, v, t~, Ko, a~, C) can be split into two parts:

  1. Consensus data: (Ko, C)
  2. Memos: (Ke, v, t~, a~)

Changing the memos constitues a soft fork. The consensus still works the same way. All of the fields there are tied to the addressing scheme in some way. The view tag is even a special category within the memos since it's basically redundant data. The outputs can be recovered even without the view tags. I don't see how tying v to the addressing scheme would be a problem when Ke and t~ are already tied to it.

Practically speaking, I find it most likely that we end up using keccak for the view tag derivation, in which case we might include (Ko, C) for free in the calculation anyways.

@tevador
Copy link
Author

tevador commented Feb 2, 2022

I found another open source assembly implementation of x25519: https://github.com/kn-cs/x25519

I benchmarked the fastest version that uses the mulx, adcx and adox instructions on my machine and the DH secret can be calculated in ~120 000 cycles, which is roughly 30% faster than the SUPERCOP ed25519 implementation. I think this is very close to the ultimate performance that can be achieved with modern CPUs. My 8-core CPU would in theory be able to check 1 million outputs in about 4 seconds.

This is another advantage of using x25519 for DH - as a standard algorithm, there are more optimized implementations than for the non-standard use of ed25519.

@j-berman
Copy link

j-berman commented Feb 2, 2022

@tevador perhaps reason to try benchmarking an optimized assembly ed25519->x25519 then x25519 variable base scalar mult, for the current view tag PR?

@tevador
Copy link
Author

tevador commented Feb 3, 2022

@j-berman I think it's out of scope of the current view tag PR. It would be a relatively large change with thousands of lines of new cryptographic code. Also the ed25519 -> x25519 conversion costs 30-40% of the performance gain. It's best to leave this optimization for Seraphis and use x25519 without a conversion.

As for the Jamtis view tag calculation, after considering the available options, I think the best solution would be to use Blake2b set to output a 1-byte hash and calculate the view tag as: v = blake2b1("view-tag" || Kd || Ko || C).

  1. Blake2b is a cryptographic hash with the same security properties as Keccak, so it's more likely to pass review than Siphash.
  2. Blake2b is 4 times faster than Keccak (on my machine 750 cycles vs 3200 for Keccak). This would reduce the time to hash from 2.6% to 0.6% of the DH calculation.
  3. We already use Blake2b in RandomX and the hash is also available in libsodium. It would literally be a 2-line change to swap Keccak for Blake2b.
  4. Blake2b natively supports domain separated hash sizes from 1 to 64 bytes. Truncating keccak-256 to 1 byte is non-standard.
  5. We can include (Ko, C) in the hash "for free" (blake2b hashes 128-byte blocks).

@UkoeHB
Copy link

UkoeHB commented Feb 3, 2022

It would literally be a 2-line change to swap Keccak for Blake2b.

Not quite 2 lines, since my jamtis hash functions implementation uses a common base hash function. Would you want to use Blake2b for all of the jamtis hash functions?

@tevador
Copy link
Author

tevador commented Feb 3, 2022

Would you want to use Blake2b for all of the jamtis hash functions?

That's also an option. Blake2b is actually more suitable as a KDF because it has a natively supported keyed mode. The current specs sort of emulate a keyed mode for keccak with the Pad136 function.

@j-berman
Copy link

j-berman commented Mar 10, 2022

@UkoeHB @tevador, the paper on DLSAG mentions:

With the use of DLSAG and the new
key image mechanism, we introduce a new privacy implication in the Monero
blockchain. In particular, given two rings and their corresponding signatures,
the sender can determine whether the two truly spent public keys belong to the
same user (i.e., the two public keys where derived from the same stealth address
with randomness provided by the sender herself).
(pg. 31)

...However, Alice can only determine when Bob starts spending one step
in the future, and after that she should not know when Bob will spend again.
Thus, one way to mitigate that problem is to require Bob to generate another
dual address and move all money received to the new address.This adds the
need for another transaction, but it enables payment channel and off-chain payments,
thus paving the way to reduce the overall number of on-chain transactions. It
would be an interesting future work to determine what other privacy implications
are there when combining DLSAG with Monero, and whether different stealth
address schemes and key image definitions exist that would avoid this issue.
(pg. 32)

Throwin it out there, maybe there's a chance the updated address scheme is more workable with DLSAG to avoid this privacy implication?

@UkoeHB
Copy link

UkoeHB commented Apr 25, 2022

I think the only viable option that preserves privacy would be to download all key images since the wallet birthday. @tevador

@j-berman came up with an optimization: only download key images for txs that have outputs that pass the view tag test. We can add a 'dummy' self-send enote type to ensure that all txs have a self-send output (other than collaboratively funded txs, which are a special case that may actually never get implemented/used). Since all self-send outputs will pass the view tag test, all your key images will be sent by the remote server.

In practice, only multi-output transactions with 0 change and no self-spends will gain an extra output, compared to if we don't add dummy self-sends. Most txs will either have non-zero change, or be single-output zero-change (due to the 2-output minimum, such txs require a dummy anyway). The great thing about self-sends is we can use the encrypted address MAC to filter on type, so the dummies will be invisible to users.

There are two disadvantages.

  • A sweep to new wallet will always be picked up by the scanning service (unless you design a wallet to explicitly avoid making self-send dummies). This can be combined with analyzing ring membership vs the set of view-tag flagged enotes to increase the likelihood of identifying those sweeps (although they will be indistinguishable from normal txs paying out of the original wallet).
  • Clients of a remote-scanning service need a back-up solution for key image recovery just in case some user does collaborative funding or makes 0-change txs with a hacked wallet that doesn't add dummy self-sends (or a wallet that fails to implement them properly).

A 56 bit address space still allows over 7.2 x 1016 addresses per wallet, which is more than enough for any imaginable use case. @tevador

I want to open a discussion about increasing address tags from 56 || 8 bits for j || MAC to 128 || 16 (8 bytes to 18 bytes).

Pros

  • Future-proof address indexing. With this many bits in the index, it is extremely unlikely that anyone will successfully advocate for more bits (less risk of a future hard fork).
  • Future-proof encrypted address index MACs. A two-byte MAC on top of 1-byte view tags would be sufficient to optimize scanning costs at any plausible scale of the chain. For example, 1mill enotes at 100ns per blowfish cipher is 100ms. With a 1-byte MAC, you need to fully scan 3.9k of those (need to benchmark it, but probably on the order of 1-5 seconds). A 2-byte MAC would bring the average cost of full scanning in-line with the cost of blowfish ciphers. I will follow up with an actual perf test.
  • Random address generation is the main use-case for 16-byte address indices. If users randomly generate addresses with the expectation of no collisions, either for privacy reasons or to satisfy a business requirement (e.g. a UUID system), then they should not get collisions. A 7-byte address index has a high enough chance of collision that it could realistically affect some user somewhere given enough time (we are trying to build something that will last a very long time). It would be great if 'generate random address' were a robust and widely-adopted user experience (no more user-managed addresses, no more thinking about 'should I reuse this address or try to make a new one'). Note that using random addresses also improves the privacy of remote scanning (which can see the nominal spend key and decrypted address tag of their users' owned enotes).
  • Aesthetically, 7-byte address indices are unappealing, and unpleasant to deal with in-code (just throwing that out there :p).

Cons

  • Addresses and tx outputs would be 10 bytes larger. This is around 10% for addresses and outputs, and up to 1% for txs (per output).
  • It might be overkill/bloat with no practical effect.

A 16|2 solution would leave me feeling confident it is solid and will be completely effective for any users' needs. The cost seems pretty trivial compared to peace-of-mind.

Cipher of 18 bytes

Blowfish operates on 8-byte blocks, but I think we can do an 18-byte block with 3 cipher operations, where the second two cipher operations overlap. However, if the first 64 bits of two address indices are the same, then the first 64 bits of their address tags will be the same. To avoid that issue, we can use (EDIT: Twofish) with 2 cipher operations.

Here is a sketch. I use an adjusted CBC mode (CBC is Cipher Block Chaining, which lets you cipher many blocks in a row by just XORing each plaintext block with the previous ciphered block before ciphering it). Since the second cipher operation overlaps with the first, I only XOR the parts that don't overlap.

// cipher
uchar tag[18];
tag[0..15] = j;  //address index
tag[16..17] = mac;

CIPHER_CTX context;
Init(context, cipher_key);
Encrypt(context, tag[0..15]);
tag[16..17] ^= tag[0..1];
Encrypt(context, tag[2..17]);

// decipher
uchar deciphered_tag[18] = tag;
Decrypt(context, deciphered_tag[2..17]);
tag[16..17] ^= tag[0..1];
Decrypt(context, deciphered_tag[0..15]);

When deciphering, you can skip the second Decrypt() call if the MAC isn't zero (the cipher is only used for making normal address tags - not any self-send stuff).

@UkoeHB
Copy link

UkoeHB commented Apr 28, 2022

I discovered one small privacy issue around 'isolated enotes'. If person A makes one enote and gives it to person B to build a tx around, person B will make a 'special change' output that re-uses the original enote's ephemeral pubkey (the special 2-output rule). If person A makes another enote and gives it to person C (who is actually an alias of person B), then if C uses the same change address when constructing a tx around the second enote, then the two change outputs in the two txs will have the same onetime address (the 'ephemeral' key isn't ephemeral if it's used multiple times).

EDIT: A bigger problem with this is the ability to burn the funds of person C (a onetime address can only be used to spend one enote).

The easiest mitigation to this is always using a random change address index, so the change output will always be different (and also a random dummy address index for the rare dummy self-send cases). Another solution would be to add an empty dummy enote in these cases, so the local signer will always make a change enote that isn't coupled to the other person's enote.

@UkoeHB
Copy link

UkoeHB commented May 17, 2022

Right now, self-send jamtis enotes are constructed by mangling the address tag MAC for different self-send types. In this design, if you try to make a 2-out transaction from two self-send types (owned by the same wallet), then those outputs will have the same onetime address if the underlying recipient addresses are the same. If the underlying addresses are different, then subtracting the onetime addresses will equal the difference between the underlying addresses (because the onetime addresses' sender extensions will be the same). Similar problems apply to the view tags and amount commitments.

The existing solution is to ban 2-out txs with two self-send enote types. The cost of this is any tx with a self-spend and non-zero change will have at least 3 outputs, which makes such txs (e.g. churn txs) more distinguishable than otherwise (most txs on-chain have only two outputs).

An alternative solution is to bake the self-send type differences into the sender-receiver secret q. This is easily done with separate domain separators for each type.

const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELF_SEND_ENOTE_DUMMY[] = "jamtis_self_send_enote_dummy";
const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELF_SEND_ENOTE_CHANGE[] = "jamtis_self_send_enote_change";
const constexpr char HASH_KEY_JAMTIS_SENDER_RECEIVER_SECRET_SELF_SEND_ENOTE_SELF_SPEND[] = "jamtis_self_send_enote_self_spend";

With this approach, it is only necessary to ban two self-send outputs of the same type. That case will basically never occur in practice (barring an implementation error), because most double-self-send txs will be a self-spend (e.g. churn) + change.

@UkoeHB
Copy link

UkoeHB commented May 23, 2022

As discussed here, it is possible to mess with the recipients of funds by sending multiple enotes to the same onetime address (by using the same send-receiver secret and destination address). Only one of those enotes is spendable, which violates the principle of least astonishment. Practically speaking, it creates a lot of headaches for multisig protocols and is a difficult question to reason about for any off-chain tx building protocol, and means any payment validator must keep a full record of all past transactions received to the corresponding wallet (directly related to jamtis).

The solution recommended by @kayabaNerve, and which I have since implemented in my Seraphis proof-of-concept, is to bake a hash of some unique 'input context' into the sender receiver secret.

  • Normal transactions: input_context = H("domain-sep", {sorted input key images})
  • Coinbase transactions: input_context = H("domain-sep", varint(block height))
  • sender-receiver secret (plain): q = H("domain-sep", K_d, input_context)
  • sender-receiver secret (self-send): q = H[k_vb]("domain-sep", K_e, input_context)

The cost to seraphis is reduced transaction modularity, making collaborative funding much more difficult and isolated enotes impossible. However, the benefit in terms of protocol health/robustness are worthwhile in my view (protocol health/robustness is a higher priority than fancy features).

@UkoeHB
Copy link

UkoeHB commented May 26, 2022

There is a slight issue with plain sender-receiver secrets. If a data-provider (remote node, remote scanner, etc.) gives you a K_e value with torsion element, then plain sender-receiver secrets can be reproduced but self-send secrets cannot. This could allow a data-provider to fool a user into only being able to find incoming transfers, but not self-sends.

I will change it to the following: sender-receiver secret (plain): q = H("domain-sep", K_d, K_e, input_context)

@UkoeHB
Copy link

UkoeHB commented Jun 14, 2022

Follow-up on the fund-burning comment: the issue still exists for users relying on third-parties (remote nodes or remote scanners) who can send the user funds with in invalid input_context (i.e. duplicated from another enote owned by the user). The third-party can send the user a malicious enote with very tiny amount and duplicated onetime address (and invalid input_context). If that enote is spent, then the original enote will be unspendable.

To mitigate this, I will update one-time addresses to include a hash of the amount commitment. This way, to burn funds you have to at least burn exactly the same amount as the real enote sent to the user. It also eliminates a sub-class of this attack where the malicious third party doesn't know the amount in the real enote (nor the amount commitment's blinding factor).

Incidentally, this resolves my disagreement with @tevador about including C in the view tag (it will be transitively included via the one-time address).

@UkoeHB
Copy link

UkoeHB commented Jun 17, 2022

In the current version of Jamtis, a find-received service and generate-address hub can combine to create a payment validator. This seems suboptimal, so I'd like to add an internal private key to the key structure.

unlock-amounts key: k_ua = Hn(k_vb)

K_1 = unchanged
K_2 = k_fr * K_3 (unchanged)
K_3 = k^j_a * k_ua * G

  • The payment validator needs s_ga, k_fr, k_ua, k_vb X + k_m U to view incoming funds.
  • The generate-address hub needs s_ga, k_ua G, k_fr k_ua G, k_vb X + k_m U to make addresses.

I am presenting balance recovery at the Monerokon this weekend, so hopefully this last-minute change will work out ok.

@tevador
Copy link
Author

tevador commented Jul 19, 2022

I want to open a discussion about increasing address tags from 56 || 8 bits for j || MAC to 128 || 16 (8 bytes to 18 bytes).

I think this is most likely an overkill.

  1. We only need to future-proof the design to match the expected lifetime of the cryptographic primitives used by Monero (Curve25519, Keccak-256), which is less than 100 years. For example, the 48-bit MAC addresses have a design lifetime of 100 years according to the IEEE (source, page 13). Our address index is 8 bits more and doesn't need to be globally unique. 56 bits is plenty.
  2. The performance benefit of a 16-bit MAC is negligible. For examples, if the shared secret calculation takes 40 μs, Blowfish cipher 100 ns and the remainder of the output recovery 1 ms (exaggerated on purpose), then you'll get the following scanning times per output:
optimization avg time per output
none 1040100 ns
8-bit view tag 43907 ns
8-bit view tag, 8-bit MAC 40016 ns
8-bit view tag, 16-bit MAC 40000 ns

The 16-bit MAC saves negligible 16 ns per output (0.04%).

  1. Random address generation sounds appealing, but IMO not enough to justify the increased address and transaction size. Collisions can be easily avoided by partitioning the 56-bit space in a similar way as the 48-bit MAC address space is partitioned (e.g. you can have 17M independent generators if you assign a 32-bit space to each of them).
  2. Working with the (7+1)-byte indices in the code is probably less complicated than encrypting the (16+2)-byte index that doesn't match the block size of any block cipher.

@UkoeHB
Copy link

UkoeHB commented Jul 19, 2022

  1. "For example, the 48-bit MAC addresses have a design lifetime of 100 years according to the IEEE (source, page 13)." This citation is not very encouraging, considering they spend two pages talking about how to optimize use of the address space.
  2. I agree the "performance benefit of a 16-bit MAC is negligible" when talking about a full-node scanner whose computation costs will be dominated by the view tag check. I disagree when talking about the client of a remote scanner. Using your numbers, an 8-bit MAC means the post-MAC check will be 39x more expensive than the cipher on average (in practice it's probably closer to 4x). With a 16-bit MAC, it would be 0.15x on average (~0.02x in practice). My goal is for the cipher to dominate costs for the client of a remote scanner.
  3. Tbh, I am really not enthusiastic about a bunch of spaghetti all over the ecosystem that exists just to handle the address index being too small for blind random generation. This just makes Monero harder to use confidently.
  4. Yeah it's more complicated - but only like 50 lines of code more complicated than 8 bytes.

@tevador
Copy link
Author

tevador commented Jul 19, 2022

  1. My point was that MAC addresses work with a global 48-bit space. We'd have a 56-bit address space per wallet.
  2. Thanks for the clarification. Do we expect 3rd party scanners to be common? Does saving 5 seconds per wallet sync for a subset of Monero users justify the costs?
  3. You can generate about 380 000 random 56-bit indexes before the collision chance exceeds 10-6. Merchant wallets are probably the only ones who would benefit from a larger index space, which would allow a simpler implementation for them. Does this justify the costs of having a larger index for all users?

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