Skip to content

Instantly share code, notes, and snippets.

@tevador
Last active December 10, 2024 20:03
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))
@jeffro256
Copy link

Current Seraphis/Jamtis design improves this flow significantly. The hot wallet is a ViewBalance tier that can see everything and allows the user to safely prepare an unsigned transaction, including the exact list of enotes to spend and all outputs. Then there is one interaction with the cold wallet, which only displays the most basic information for confirmation and then sends the ownership proofs to the hot wallet. The hot wallet then performs decoy selection, completes the membership proof and submits the transaction to the network.

This new flow would still be possible with almost exactly the same operations if you separate one-time address recovery and amount recovery with different keys.

I don't see a strong case to support the additional tiers in the official specs.

I think this might be tied to the following argument you make later:

The setup with a ViewBalance main wallet and a signing hardware wallet has a much more appealing UX and achieves the main goal why users purchase hardware wallets: theft protection.

For the same reason why one would want theft protection, one would want to hide amounts: mitigating compromised internet-connected computing devices. If you are working hard to mitigate this threat, I don't see why it would be such a leap to assume that some wouldn't also want to also hide the balances from their assumed-to-be compromised devices, given the option.

Hardware wallets have a tiny screen and a few buttons, which are enough to display and confirm the intent, but are completely inadequate for a full wallet interface

They wouldn't have to implement any interfacing, just the transaction input selection algorithm, which is neither or a compute heavy task nor memory heavy. At any rate, this also wouldn't be a problem for cold wallets using a normal laptop/desktop/smartphone.

@jeffro256
Copy link

jeffro256 commented Sep 18, 2023

All of this must be part of the specs to avoid fragmentation of the Monero ecosystem. This way, a user can restore their mnemonic seed into any compliant wallet implementation and will see the correct balance.

The key word here is compliant. Thus far we've been trying to mitigate people moving off of a compliant wallet design to something more suitable. The only way we can do this is by using incentives, which is why it's my opinion that its fruitless to base the privacy off of doing optionally doing something with your keys, given that you can interact with the ecosystem all the same. As such, telling users to not multiply by some d_ua factor is merely a recommendation, and we should not expect the ecosystem to evolve in that manner. And since, IMO, separating the one time address recovery and amount recovery in self-send enotes provides a tangible benefit for a real use case without affecting the performance of others, we should move forward with that feature.

@jeffro256
Copy link

Regardless of the tag_size, the view tag is always encoded in 2 bytes as a 16-bit integer per enote. The remaining bits are filled with a "complementary" view tag calculated from s^sr_1, which needs a different private key.

I like the complimentary view tag as long as s^sr_1 depends upon DH_2 only, and not DH_1. The only difference I would make here is that since the length of the view tag needs to be encoded anyways with 4 bits, we can extend the complimentary view tag by 4 bits to squeeze extra performance out of the complimentary view tag without increasing transaction sizes. As for the table you provided, if we use the technique of encoding the enote type into the blinding factor, we only have to recalculate K_o once for any enote.

@tevador
Copy link
Author

tevador commented Sep 18, 2023

one would want to hide amounts

Ideally, one would want to hide everything.

Users can just share d_fr with the hot "wallet" and scan the remaining 200 KB/day on the cold wallet. Assuming a USB 2.0 interface between the hot and the cold wallet, you can easily download several months of matches per second. This way, the hot wallet doesn't even learn which outputs you own. The bottleneck would be the DH calculation in the hardware wallet, but even tiny Cortex M4 can do a few hundred per second (1-2 minutes per month of enotes, bearable for this level of privacy).

They wouldn't have to implement any interfacing, just the transaction input selection algorithm

Most wallets nowadays allow for manual input selection, which needs an interface that lists all unspent inputs and their amounts.

The key word here is compliant.

I'm pretty sure that most wallet implementations will follow the specs. "Non-compliant" is not something you want to explain to your users.

As such, telling users to not multiply by some d_ua factor is merely a recommendation, and we should not expect the ecosystem to evolve in that manner.

IMO, "users might do it anyways" is not a valid reason to put something in the specs.

The original reason why d_ua was added is explained in this comment:

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.

This issue no longer applies to the proposed scheme, so I don't see a reason to keep it. Having a separate wallet tier that can identify all owned enotes without amounts facilitates the existence of scanning services that harm users' privacy. Remote scanners that don't have to deal with decoy enotes have much smaller storage and bandwidth requirements, so I see this wallet tier as the most likely one to be used. Unfortunately, it also makes the dynamic tag feature and the extra pubkey in each address redundant.

The only difference I would make here is that since the length of the view tag needs to be encoded anyways with 4 bits, we can extend the complimentary view tag by 4 bits to squeeze extra performance out of the complimentary view tag without increasing transaction sizes.

It would actually increase transaction size a tiny bit because you would need an extra byte per output, while the current proposal only needs a byte for the whole tx. I'm also not sure if allowing different view tag sizes for every input is a good idea for tx uniformity.

As for the table you provided, if we use the technique of encoding the enote type into the blinding factor, we only have to recalculate K_o once for any enote.

At least twice, because self-sends have a different shared secret.

@jeffro256
Copy link

As for the view tag filter target, when you say "target" here, do you mean in the sense that it is a relay-enforced minimum amount of filtering to do?

The filter target should be 480 enotes/day. Because the view tag filter rate must be a power of 2, this will actually result in a range from 480 to 960 enotes per day depending on the tx volume. If we "average the averages" over all possible values of tx volume, this will give a mean of 720 enote matches per day, or roughly 1 match per block, which is what was suggested by @jeffro256. I think this is close to the upper limit of what is acceptable for light wallet clients (~200 KB/day) and should provide a good number of false positives even if there was a short term drop in tx volume.

The fomula to calculate the view tag size in bits is:

tag_size = trunc(log2(3 * num_outputs_100k / 200000))

I think we should also enforce a maximum size as well for uniformity purposes, make the minimum not so lax, while also allowing wiggle room for fluctuation between signing and propagating. Let's say that we want to plan for a fluctuation of 25% in the value of num_outputs_100k between signing and submitting. Nodes could do the following:

min_relay_tag_size = round(log2(num_outputs_100k * 3 / 4 / 100000))
max_relay_tag_size = round(log2(num_outputs_100k * 5 / 4 / 100000)) 

The wallets would do the following:

wallet_tag_size = round(log2(num_outputs_100k / 100000))

When it came time for transaction verification, nodes would check that min_relay_tag_size <= wallet_tag_size <= max_relay_tag_size.
The worst case scenario for a wallet is that between signing and propagating, the enote volume over the last 100k blocks goes up or down more than 25%, and the transaction does not enter the mempool and has to be edited. Also, round(log2()) can be implemented with integer instructions portably as well, but its a little more complicated.

@tevador
Copy link
Author

tevador commented Sep 18, 2023

As for the view tag filter target, when you say "target" here, do you mean in the sense that it is a relay-enforced minimum amount of filtering to do?

No, I mean this is the target we plug into the formula. You can see that the bitsize is calculated for 2/3 of matches per block. If we could have fractional bits, the target of 480 enotes/day would be exact. The range 480-960 is an artifact of rounding down.

wallet_tag_size = round(log2(num_outputs_100k / 100000))

Since round(log2(num_outputs_100k / 100000)) = trunc(log2(num_outputs_100k / 100000)+0.5) = trunc(log2(sqrt(2) * num_outputs_100k / 100000)), you'll effectively get a range of from 720/sqrt(2) to 720*sqrt(2), which is about 509-1018. Is there a benefit to this compared to the range 480-960 I'm proposing?

I think we should also enforce a maximum size as well for uniformity purposes

My idea was that nodes would use the same formula, but would apply the formula at each of the last let's say 10k blocks and build a range based on that. Effectively, this would give you all possible view tag sizes from the last 10k blocks as valid values. Most often, this would just be a single value. This means transactions would never be invalidated unless they took more than 10k blocks between signing and propagation.

@jeffro256
Copy link

It would actually increase transaction size a tiny bit because you would need an extra byte per output, while the current proposal only needs a byte for the whole tx. I'm also not sure if allowing different view tag sizes for every input is a good idea for tx uniformity.

Okay yes, this is a much better idea... I was assuming that view tag sizes were going to be per-enote,

At least twice, because self-sends have a different shared secret.

True, my bad

Ideally, one would want to hide everything.

Users can just share d_fr with the hot "wallet" and scan the remaining 200 KB/day on the cold wallet. Assuming a USB 2.0 interface between the hot and the cold wallet, you can easily download several months of matches per second. This way, the hot wallet doesn't even learn which outputs you own.

This is a really excellent reason to not include d_ua: the ones who are paranoid enough to do this type of setup would probably be willing a couple extra minutes per month of DH calculations. Okay, I'm sold on keeping out d_ua for now.

@jeffro256
Copy link

My idea was that nodes would use the same formula, but would apply the formula at each of the last let's say 10k blocks and build a range based on that. Effectively, this would give you all possible view tag sizes from the last 10k blocks as valid values. Most often, this would just be a single value. This means transactions would never be invalidated unless they took more than 10k blocks between signing and propagation.

This covers almost everything except for slight future changes. There's three edge case scenarios in which honestly built transactions might fail to propagate on the network:

  1. The wallet's node is ahead of its other connected nodes, and its current tag_size value is a previously unseen value because of a slightly different num_output_100k. The tx will be accepted on this node, but not propagated to other mempools (depending on the exact p2p rules, this issue might be mitigated b/c nodes won't share mempool information until they are synced to the same chain height).
  2. Same as scenario #1, but the inconsistency of this node with the network is due to a reorg
  3. The node gives a wallet a num_output_100k value, then reorgs, then the wallet submits a tx with an invalid num_output_100k. The tx will not enter any mempool, but at least this time, the user will get an error message.

In addition, this solution doesn't require storing a history of allowed values (although you could make the history very small with O(1) access using a map of values -> number of instances of that value).

you'll effectively get a range of from 720/sqrt(2) to 720*sqrt(2), which is about 509-1018. Is there a benefit to this compared to the range 480-960 I'm proposing?

Not necessarily, although the range is biased towards a slightly higher degree of privacy.

@tevador
Copy link
Author

tevador commented Sep 19, 2023

The wallet's node is ahead of its other connected nodes, and its current tag_size value is a previously unseen value

I don't think we need to handle this case. It can already happen with decoys, which might still be invalid (locked) for other nodes that are behind. I don't know how it's handled currently.

reorg

This can be solved by shifting the calculation 10 blocks to the past, i.e. we will use the range [chain_tip-100009, chain_tip-10] instead of [chain_tip-99999, chain_tip] to calculate the view tag size. If there was a reorg deeper than 10 blocks, transactions could be invalidated anyways due to invalid decoys.

the range is biased towards a slightly higher degree of privacy

Actually, it is biased towards a lower degree of privacy.

The two formulas can be approximated as follows:

trunclog2(3 * x / 200000) ~ trunclog2(x / 66667)
roundlog2(x / 100000)     ~ trunclog2(x / 70711)

These approximations are accurate to within 0.01%, which is more than enough for the intended use case. trunclog2 is a very simple function that returns the index of the most significant one-bit. It can be calculated by repeated shifting and some CPUs even have a dedicated instruction for it (x86 BSR).

I ran the numbers for the two distributions and here are the results (converted to the number of false-positive matches per day):

formula min max mean st. dev. median
trunclog2(x / 66667) 480.0 960.0 720.0 138.6 720.0
trunclog2(x / 70711) 509.1 1018.2 720.0 144.8 689.1

The second formula has median lower than the mean, which means it's skewed towards smaller values. It makes sense if you think about it: both distributions have the same mean of 720 but the second one has a higher min and max, so it must be skewed.

@jeffro256
Copy link

jeffro256 commented Sep 19, 2023

If we can accept a worst case filtering deviation of 2x, versus 1.5x or 1.41x with our current ranges, then we can get a best-case aggregate deviation of 0x with user cooperation using the following scheme. For any given value of num_output_100k, consensus rules allow wallets to choose between two values of view tag filter: t_1 = trunclog2(num_output_100k / 1000000) and t_2 = t_1 + 1 (your stored history method can be used here or not). The wallet gets to choose between these two values, and the general idea is that they will pick between these two values in a ratio that makes the aggregate filtering rate close to 720/day, no matter what the value of num_output_100k is. This is how it is done:

Our aggregate filtering rate is can be defined as follows, where v is enote volume per block (this is to make calculations simpler, practically we would set v = num_outputs_100k / 100000 or some other way of smoothing this value):

F(v) = v / (w(v) * p(v) + (1 - w(v)) * 2p(v))

where p(x) = 2 ^ trunclog2(x) [the greatest power of 2 less than or equal to x]
     and v = enote volume per block
     and w(v) is a weight function with values [0, 1] between choice of tag size t_1 and t_2 for a given v

Ideally, we want F(v) = 1 (aggregate filtering rate is 1 enote per block). If we set F(v)=1 and solve for w(v), we get:

w(v) = 2 - v/p(v)

So to pick between between t_1 and t_2, wallets will generate a random value c in range [0, 1]. If c <= w(v), then wallets will pick t_1, else they will pick t_2.

This will get us close to the ideal filtering rate, 720 enotes/day, no matter what num_outputs_100k is, assuming most people cooperate. If we don't assume that, the aggregate filtering rate can swing anywhere from 360 to 1440 enotes/day.

@jeffro256
Copy link

We could optionally make the choice c deterministic as a function of num_output_100k and input_context, which would maybe increase uniformity while having the same effect for the aggregate filtering target. The downside is transactions would be a couple bytes bigger because we would need to encode num_output_100k instead of t_1 or t_2. Also, if a user can choose multiple values for input_context (e.g. by changing their ring member set), then they could brute-force c. However, that's a lot of work to encode 1 bit worth of information; there's already much, much more efficient ways to encode arbitrary data. Overall, probably not worth making c deterministic, but that's always an option.

@tevador
Copy link
Author

tevador commented Sep 20, 2023

Here is a better method that follows the target of 720 enotes/day and doesn't rely on user cooperation.

Global parameters

There is a single global parameter:

filter = max(1, 6553600000 / num_outputs_100k)

It's an integer between 1 and 65536 and should be specified in every transaction. This needs 2 bytes per tx, but it still has a slightly smaller blockchain footprint than the current Jamtis spec:

# of tx outputs current spec proposal
2 6 bytes (2x view tag, 2x tag hint) 6 bytes (1x filter, 2x view tag)
16 48 bytes (16x view tag, 16x tag hint) 34 bytes (1x filter, 16x view tag)

Enote parameters

Every enote has two "fingerprints":

  1. fingerprint1 = H("jamtis_fingerprint1" || DH_1 || K_o) % 2^16
  2. fingerprint2 = H("jamtis_fingerprint2" || s^sr_1) % 2^64

fingerprint1 is a 16-bit integer and fingerprint2 is a 64-bit integer.

View tag derivation

The 16-bit view_tag is calculated as follows:

view_tag = (fingerprint2 % filter - fingerprint1) % 2^16

Checking for a match

The view tag is checked with the following condition:

(fingerprint1 + view_tag) % 2^16 < filter

For non-owned enotes, (fingerprint1 + view_tag) % 2^16 is a uniformly distributed random number, so it will match for filter/65536 enotes on average, which simplifies to 1 enote per block if we substitute the definition of filter.

For owned enotes, (fingerprint1 + view_tag) % 2^16 equals to fingerprint2 % filter by construction, which is always less than filter. Additionally, if we know fingerprint2, we can eliminate all but 1/filter false matches by checking:

(fingerprint1 + view_tag) % 2^16 ?= fingerprint2 % filter

This gives an overall false positive rate of 1/65536 for wallets that are able to calculate both fingerprints.

@tevador
Copy link
Author

tevador commented Sep 20, 2023

I was interested to see how the dynamic view tags work in practice, so I took real blockchain data (the number of RingCT outputs in every block) from about March 2018 to August 2023 (blocks 1519796-2959795). It's a total of 1440000 blocks, interpreted as 2000 days times 720 blocks per day (in reality it was about 2004 days).

Method 1

The first method uses the "discrete" view tags based on the formula:

view_tag_size = trunclog2(num_outputs_100k / 66667)

view_tag_method1.png

We can see that the view tag size nicely follows the long-term trend of growth, growing from 4 bits in 2018 to 6 bits, with a few periods of 7 bits during high transaction volume in 2022. Around day 1700, fluctuations in the view tag size occur for about 4 days, which is the time when the explicitly encoded size would come handy.

daily_matches_method1.png

The number of false positive matches roughly follows the target of 720/day, with some significant fluctuations. Around day 400, the daily matches shot to over 2000/day, while the lowest number of matches recorded is 231 on day 1624. The average over the whole period is 780 matches/day.

Method 2

The second method uses the "smooth" view tag method described in the previous comment.

view_tag_method2.png

Here the filter rate tracks the long-term trends much more precisely.

daily_matches_method2.png

However, if we look at the number of false positive matches, there is not a huge qualitative difference from the first method. It follows the target of 720/day slightly more closely, but short-term tx volume fluctuations still make it deviate quite far. Around day 400, the daily matches also exceed 2000. The lowest number of matches is 301 on day 960. The average over the whole period is 762 matches/day.

Conclusion

The "smoother" methods of following the daily target are not much better than the simple discrete view tag due to short-term tx volume fluctuations. I therefore think that we should adopt method 1, which is simpler and better for tx uniformity.

@jeffro256
Copy link

view_tag = (fingerprint2 % filter - fingerprint1) % 2^16

Damn, that's clever! I don't know if it was intentional, but here's a cool feature of doing the view tags this way: you can't check against the view tag using only knowledge of fingerprint2; fingerprint1 acts as a random "mask" to the fingerprint2 % 2^16 value. What this means for scanning setups is that the incentive to only send d_vr instead of d_fi/d_fr is destroyed! If you only send d_vr to a light wallet server, they only thing they can do is compute s^sr_1 and nominal address tags, but they'd have to send you that information for every single enote; they can't actually weed any out. If they also had s_ct, they could 100% identify all incoming enotes, but it would require huge amounts of processing for the server (since they can't use view tags), and it still wouldn't cover self-sends.

If you were calculating fingerprint1 and fingerprint2 separately (i.e. light wallet), the server would need to send fingerprint1 unless the client wanted to do 2x DH ops instead of 1x DH ops per each filter enote, but since that's only 2 bytes, it's still much much smaller than the nominal address tag (16 bytes), which is what it would replace.

For this reason alone, I think this is the best way thus far.

Here the filter rate tracks the long-term trends much more precisely.

The "smoother" methods of following the daily target are not much better than the simple discrete view tag due to short-term tx volume fluctuations

I think the long term rate of filtering is more important anyways. When a user is scanning a small amount of volume, it won't matter much from a UX perspective if the small volume is a little bigger. It's when the scanning process would otherwise take many minutes or even hours (poor souls), that the long-term filtering rate would make a difference performance-wise.

I therefore think that we should adopt method 1, which is simpler and better for tx uniformity.

Since we ostensibly have to choose filter from a common, public, deterministic list of values, what would this non-uniformity tell us exactly? It would (maybe) reveal the time we constructed the transaction to the granularity of block-time (2 minutes). In most cases, this is already known by the nature of how transactions propagate in nodes' mempools. Discretized fees and ring member selection also leak this information. And in the future, if/when we go for FCMPs, a hash of the root of the curve tree for a given block will need to be included to verify the transaction, which would further cement for external observers when in time a transaction was constructed.

@jeffro256
Copy link

you can't check against the view tag using only knowledge of fingerprint2; fingerprint1 acts as a random "mask" to the fingerprint2 % 2^16 value

Thinking about it now, this is orthogonal to the smoothness of the view tag filtering rate, we could always include a 2-byte residue of DH_1 in the calculation of the complementary view tag.

@jeffro256
Copy link

jeffro256 commented Sep 21, 2023

With that in mind, I'd agree that doing tag_size = trunclog2(3 * num_outputs_100k / 200000) is probably best. Also, thanks for doing those simulations, that was actually really insightful!

@tevador
Copy link
Author

tevador commented Sep 21, 2023

It would (maybe) reveal the time we constructed the transaction to the granularity of block-time (2 minutes). In most cases, this is already known by the nature of how transactions propagate in nodes' mempools. Discretized fees and ring member selection also leak this information. And in the future, if/when we go for FCMPs, a hash of the root of the curve tree for a given block will need to be included to verify the transaction, which would further cement for external observers when in time a transaction was constructed.

It would leak the approximate time when the transaction was signed, which might be a long time before it's actually submitted to the network (e.g. multisig or offline-signed transactions). This might be considered to be a regression because Seraphis already allows for the membership proof to be added just prior to publishing the transaction, which removes the leaks caused by the member selection and only leaves fees as a possible leak.

It could be solved by removing view tags from the signed data and sign them later with the membership proof, but that might be problematic.

@jeffro256
Copy link

jeffro256 commented Sep 25, 2023

After working on implementing the new changes, I think the complementary view tag should be bound to DHE_2 instead of s^sr_1 (as well as a residue of the primary view tag calculation). The reason for this is that, if we make the complementary view tag a function of s^sr_1, in most cases, we will need to do the DH operation anyways, but now we're also doing 4 hash operations (derive plain s^sr_1, hash plain s^sr_1 -> complementary view tag, derive self-send s^sr_1, hash self-send s^sr_1 -> complementary view tag) instead of 1 (hash DHE_2 -> complementary view tag) for each 720 enotes/day. So in summary, this is how I think the view tag computations should go:

npbits = the number of primary view tag bits, explicitly mentioned in the transaction
ncbits = the total size of the view tag in bits - npbits
primary_view_tag || primary_view_tag_residue = H(DHE_1, K_o)
complementary_view_tag = H(DHE_2 || primary_view_tag_residue)
view_tag = primary_view_tag[first npbits] || complementary_view_tag[first ncbits]

The only downside I can think of when complementary_view_tag binds DHE_2 instead of s^sr_1 is the DLP solvers can check both view tags on self-sends transactions (versus just the primary view tag on self-sends) if they know your public address. However, they will not know the types of self-sends nor what the outgoing amounts are.

@tevador
Copy link
Author

tevador commented Sep 25, 2023

I think the complementary view tag should be bound to DHE_2 instead of s^sr_1

This has an undesirable side effect of revealing1 self-spends to anyone in possession of d_vr, e.g. a PaymentValidator tier, which would only be able to calculate primary_view_tag otherwise. Because PaymentValidator is likely to be a hot wallet with an increased risk of key compromise, I think this side effect should be avoided and the complementary_view_tag should be calculated differently for self-sends. The cost of this can be just 1 extra hash, which is negligible compared to the 2 DHE calculations that precede it.

1 Technically, it reduces the false-positive rate to 1/65536, which is about 6 false matches per week with current tx volume.

@jeffro256
Copy link

Ah shoot you're right, I wasn't thinking about that tier... nevermind.

@jeffro256
Copy link

What is the reasoning for these types? AFAICS we only need 2 types to tell the wallet if the enote should be displayed in history or not (this could also be achieved with a 1-bit flag encrypted with s^sr_2, so only 1 extra K_o recomputation is needed).

We can't do this because if you have a 2-output tx (using a shared xK_e), and one of your outputs is a self-spend and the other is a change output, Ko will be shared between the enotes which will 1) reveal that this is a tx where a user is trying to churn and 2) burn funds for one of the enotes.

So we will need to do 2x extra Ko re-computations (3 total) for each enote that matches both view tags.

@tevador
Copy link
Author

tevador commented Sep 27, 2023

We can't do this because if you have a 2-output tx (using a shared xK_e), and one of your outputs is a self-spend and the other is a change output, Ko will be shared between the enotes

This can be fixed by including the output index in the shared secret calculation. That would make both outputs have unique K_o.

In fact, the same problem applies to normal enotes. If someone sends a 2-out tx where both outputs go to the same address, both outputs will have the same K_o. This is non-standard, but AFAIK the protocol allows it. This shows that the input_context that only consists of key images is insufficient to ensure the uniqueness of K_o and fails to prevent "the burning bug".

@jeffro256
Copy link

In fact, the same problem applies to normal enotes. If someone sends a 2-out tx where both outputs go to the same address, both outputs will have the same K_o. This is non-standard, but AFAIK the protocol allows it.

The protocol doesn't allow it though. One of the rules of Jamtis is that every transaction contains at least one self-send output (for this reason, as well as allowing third-party light wallet servers to trim the key image set and give the clients access to their outgoing transactions). If you have 2 normal outputs to the same destination, and need at least one self-send, that means you wouldn't be doing the shared xK_e optimization.

@jeffro256
Copy link

Also, having K_o bound to the tx output index would be really annoying (AKA involve brute-forcing private ephemeral keys) since IIRC the enotes in Seraphis are ordered by one-time addresses.

@tevador
Copy link
Author

tevador commented Sep 27, 2023

The protocol doesn't allow it though. One of the rules of Jamtis is that every transaction contains at least one self-send output

How is this rule enforced?

Imagine the following scenario:

Mallory registers at an exchange and is provided with a deposit address. She crafts a 2-output transaction without change, sending both outputs to the deposit address, each output worth 1000 XMR. In order to do this, she needs to provide inputs with a total sum of exactly 2000 XMR + fee, but that should not be hard to do.

Unless the exchange has a wallet that is aware of the burning bug, Mallory will be credited with 2000 XMR and can proceed to withdraw the funds back to her custody. However, the exchange will later realize that only one of the 1000 XMR outputs can be spent. This scam can be repeated until the wallet of the exchange is completely drained. It only costs some tx fees.

Relying on all wallet implementations to be able to detect this bug is not going to work, so there are basically two solutions:

  1. Mandating unique K_o within each transaction as a consensus rule.
  2. Including the output index when deriving K_o.

IIRC the enotes in Seraphis are ordered by one-time addresses

Is this a consensus rule or just a recommendation for tx builders?

@jeffro256
Copy link

jeffro256 commented Sep 27, 2023

How is this rule enforced?

That rule specifically is not enforced at a consensus level, it's just a Jamtis rule-of-thumb that is derived from the Seraphis protocol consensus rule that enote outputs within a transaction should be ordered and unique by one-time address. See this code for details and implementation: https://github.com/UkoeHB/monero/blob/eeca802ccee217d26acd8bc89ee69bbd3c47e254/src/seraphis_main/tx_validators.cpp#L365-L367.

In this way, and assuming that input_context differs from transaction to transaction, all cases of the burning bug should be covered.

@tevador
Copy link
Author

tevador commented Sep 27, 2023

consensus rule that enote outputs within a transaction should be ordered and unique by one-time address

Cool. It's the first time I hear about this rule. Maybe it's worth adding it to the Seraphis specs?

The current "Implementing Seraphis" paper says the following:

To further ensure uniqueness within a transaction, transaction verifiers must mandate that all values K_e in a transaction are unique.

Uniqueness of K_e is not sufficient to prevent the burning bug as shown above.

@jeffro256
Copy link

Uniqueness of K_e is not sufficient to prevent the burning bug as shown above.

That's true and a good thing to point out more explicitly in the spec. I can open an issue on that repo to clarify that passage.

@jeffro256
Copy link

jeffro256 commented Sep 28, 2023

This brings me to an interesting privacy hiccup when distributing xk_fr to a third-party, under both the new and old schemes: depending on the size of the previous view tag / primary view tag, a third-party will see that outgoing transaction enotes are exponentially more likely to to be owned by a user the more self-send enotes there are. This affects both light wallets and people using the payment validator tier.

We can reason that the number of successful view tag checks within a transaction unrelated to you follows a binomial distribution. Each view tag check is a Bernoulli trial, so we can expect the number of successful view tag checks X for a transaction with n outputs to follow the distribution X ~ B(n, VTFP), where VTFP is the view tag false positive rate. The probability mass function for getting k view tag matches can be written as P(X = k) = (n choose k) * VTFP^k * (1 - VTFP)^(n-k). As an extreme example, someone may implement PocketChange-like feature which breaks up outputs to help users work around the 10-block lock. Let's say they create 16 self-send outputs and the false positive rate is 1/256. All 16 outputs will be matched by view tag, which should normally only have a probability of 2.939 x 10^-39 (the same chance as randomly guessing someone's AES key). This can also happen with 2-output transactions with one self-spend and one change, although not as severe: the probability should be 1/65536.

We need a way to have third-parties scan the information they need without this privacy downside. I propose that we split up the self-send types into three self-send types: SELF_SPEND, PLAIN_CHANGE, & AUXILIARY_CHANGE. When doing an outgoing transaction, enote types SELF_SPEND XOR PLAIN_CHANGE (one or the other, not both) will always be present. For these enotes, primary view tags will calculated as normal. For any additional desired self-sends, we set the primary view tag to random bits and the self send type to AUXILLIARY_CHANGE, but do everything else the same (meaning binding the self-send type to s^sr_1). When it comes time to scan, we also scan all enotes in transactions in which any of the view tags matched even if their view tag did not match (hence "auxiliary"), but only scan them for type AUXILIARY_CHANGE. Unfortunately, this change will more than double the bandwidth required for light wallet clients, but only marginally affect compute time as no extra DH ops are required, and depending on the complementary view tag size, most enotes won't have to have K_o recomputed.

@jeffro256
Copy link

jeffro256 commented Sep 28, 2023

This wouldn't have to slow down non-auxiliary enote scanning at all (besides an extra amount commitment recomputation on an already confirmed owned enote) due to the following reason: since we assume that exactly one of SELF_SPEND or PLAIN_CHANGE is present in a transaction, they can share the same s^sr_1 derivation, and only have s^sr_2 derivation differ (this avoids the problem of sharing xK_e leading to the same K_o). The s^sr_1 derivation for AUXILIARY_CHANGE would differ, which leaves us with the same number of K_o re-computations that we have to do: 1x for plain check and 2x for self-send check.

For any additional desired self-sends, we set the primary view tag to random bits...

To speed up auxiliary enote scanning, we could actually fill the primary view tag bits up with all complementary view tag bits, since we don't care about it matching anyways, but we're also going to check the complementary view tag.

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