Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] early payment proofs #70

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

tromp
Copy link
Contributor

@tromp tromp commented Oct 9, 2020

@tromp tromp changed the title initial version of early payment proofs [WIP] early payment proofs Oct 9, 2020
Payment proofs prevent a payment receiver from claiming they didn't receive payment.
Such fraud prevention is an essential ingredient to commercial adoption.

Former payment proofs used in Grin didn't apply to invcoice flow, at least not
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo invcoice -> invoice

They also lacked the ability to specify dating and purpose of payment.

This RFC changes the transaction building process where payers can require
payees to create a "proof" they've received a payment before the payer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should make it clear here that anyone can finalize it since it allows for all tx flows

# Unresolved questions
[unresolved-questions]: #unresolved-questions

* What limit is imposed on the size of the memo field?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say at least 32 bytes should be possible by default to be able to store say a sha3-256 hash of an invoice document or similar

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i set the limit to 32 bytes mentioning the room for a hash

@pkariz
Copy link

pkariz commented Oct 11, 2020

I think it would be better to replace i with R where R is a sum of public nonces of the sender and the receiver. That's because in case of a reorg i changes and becomes a false information (i guess the user would have a way to export his payment proof, so in case of a reorg the wallet would need to locate the new i and the user would need to export the proof again and delete the existing proof). I think that the payment proofs should not change over time so it's worth having their size be a bit bigger to get that property

@tromp
Copy link
Contributor Author

tromp commented Oct 11, 2020

A deep enough reorg can always invalidate a payment proof, whether it uses an index or a nonce.
It's up to the sender to make sure that their proof is valid.

@pkariz
Copy link

pkariz commented Oct 11, 2020

A deep enough reorg can always invalidate a payment proof, whether it uses an index or a nonce.

If after a reorg the tx is on chain then the proof remains the same if you use public nonce instead of index. If it's not then they need to create a new transaction which creates a new proof in both cases. Consider this case:

  1. i create a proof with index and export it
  2. i delete the wallet
  3. reorg appears which keeps my tx on chain but with different index

In this case I've lost the proof and can't get it (unless i bruteforce the kernels)

@tromp
Copy link
Contributor Author

tromp commented Oct 11, 2020

That's a good argument. The problem is that looking up an arbitrary kernel is expensive. The node doesn't maintain an index of all kernels. So to avoid making payment proof verification a DOS vector, we'd really like the payer to provide the index.
I suggest that when exporting a payment proof, the kernel commitment is exported along with the index, so that in case of a deep reorg, the payer can do the (potentially expensive) kernel lookup.

@pkariz
Copy link

pkariz commented Oct 11, 2020

I expect that in the future vast majority would be using the nimble node. Do you have any idea how to get the new index with the nimble node to generate the new proof? Also how would someone validate the proof on a nimble node?

@tromp
Copy link
Contributor Author

tromp commented Oct 11, 2020

Nimble nodes will presumably rely on full nodes providing merkle membership proofs.
Looking up a recent kernel might be free, but looking up an arbitrary kernel should cost some fee
(again, to avoid being used as a DOS vector).

@phyro
Copy link
Member

phyro commented Oct 11, 2020

Does this make it impossible to swap outputs? So if we made a transaction T where I had an output O1, would I as a receiver be able to swap O1 for O2 and adjust the offset? or would the payment proof become invalid in this case?
The reason I'm asking about this is because it seems that swapping outputs is the only way to "unlink" the outputs for people that have already seen the transaction. This seems useless on its own, but it can be a powerful privacy boosting method if used by everyone as described in https://gist.github.com/phyro/d70e0782f07d3375d8a92ebda21f58bb
The summary of the idea is that if you have an objective path of nodes to the fluff node (as is true in the objective dandelion), then you can collect the public keys on that path and do onion encryption of sequential swaps that need to be performed such that each node on the path performs a swap on your output. The interesting part is if this is the default behaviour because if everyone provides these onioned swaps, then each node receives a tx, deletes all the outputs and replaces them with new ones, the new tx is validated (and possibly aggregated) and sent to the next node that does the same again.

@tromp
Copy link
Contributor Author

tromp commented Oct 11, 2020

The payment proof commits to the receiver excess, so indeed you can compensate for any changes in or addition of inputs/outputs by adjusting the offset.

@pkariz
Copy link

pkariz commented Oct 11, 2020

I would probably add the payment proof version in the payment proof because if we ever implement a 2 step we will need a different payment proof and when i show you my proof you should know how to verify it. I feel like version seems like an easy way to achieve that. We will never be able to ignore older proofs so we will always need to be able to support verification of all versions of proofs

@tromp
Copy link
Contributor Author

tromp commented Oct 11, 2020

Good idea. I will add versioning.

@pkariz
Copy link

pkariz commented Oct 11, 2020

The payment proof commits to the receiver excess, so indeed you can compensate for any changes in or addition of inputs/outputs by adjusting the offset.

I don't think you can perform output swapping because to do that the receiver would need to change his blinding factors but you commit to them in your proof (receiver could technically do it if he had at least 2 elements of tx - eg. 2 outputs, but can't do it if he only has one). It does feel like output swapping might be useful when we will try to increase the privacy

Edit: no, even with 2 or more inputs you can't do output swapping because you can figure out which they were if you know the sum point

@tromp
Copy link
Contributor Author

tromp commented Oct 12, 2020

his blinding factors but you commit to them

The receiver only commits to the excess, not any blinding factors.

Btw, I don't quite see the point in output swapping. Once the tx is broadcasted, the swapped outputs are right there for the counter party to see, unless you got stem-phase aggregation.

@pkariz
Copy link

pkariz commented Oct 12, 2020

The receiver only commits to the excess, not any blinding factors.

Yes that's why he can't change blinding factors later (he could if he has 2 elements but it's pointless because you can figure out which things were changed).

Btw, I don't quite see the point in output swapping. Once the tx is broadcasted, the swapped outputs are right there for the counter party to see.

It could be used before it was broadcasted, eg. in stem phase or you could have a chain of 2 services, aggregator --> swapper and swapper would then swap stuff. I haven't thought much about it since we are not improving privacy now but to me it sounds interesting

@tromp
Copy link
Contributor Author

tromp commented Oct 12, 2020

They can change blinding factors along with the kernel offset, which preserves the excess.

Btw, I don't quite see the point in output swapping. Once the tx is broadcasted, the swapped outputs are right there for the counter party to see, unless you got stem-phase aggregation.

Once you have good aggregation, then the need to hide your output from your counterparty is much reduced, since your spend of it will be well obscured by aggregation.

@pkariz
Copy link

pkariz commented Oct 12, 2020

They can change blinding factors along with the kernel offset, which preserves the excess.

sr = rr + e*xr --> does xr here contain offset? If yes then you're right, if no then changing blinding factors would invalidate the proof if i understand correctly

Once you have good aggregation, then the need to hide your output from your counterparty is much reduced, since your spend of it will be well obscured by aggregation.

Counterparty knows your inputs/outputs (well at least the finalizer does) and aggregation can't fix that

@tromp
Copy link
Contributor Author

tromp commented Oct 12, 2020

The RFC has this equation

  1. Verify that s*G = R + e*X, where e is the hash challenge of the i'th kernel.

which has no blinding factors, only receiver's excess X.

Counterparty knows your inputs/outputs (well at least the finalizer does) and aggregation can't fix that

You missed my point. Aggregation makes it much less useful for counterparty to know your output.

@phyro
Copy link
Member

phyro commented Oct 12, 2020

Once you have good aggregation, then the need to hide your output from your counterparty is much reduced, since your spend of it will be well obscured by aggregation.

True, but it won't be obscured from anyone seeing the tx prior to aggregation. Output swapping in its extreme version means that if you give me a tx T with output set S, I'll give you back tx T' with output set S'. If you had all the information in the world about the outputs in T you have no information about the outputs in T'.

Btw, the swapped outputs are not public for anyone to see - this would not give any privacy. This is why I merged the idea with objective dandelion which has for a given node N1 an objective path to the fluff node Nf. The hiding of swapped outputs is then done with a simple onion encryption that gives each of the node exactly 1 description of swapping of each output. Then each of the nodes does what I described above, gets tx T with output set S, replaces S with S' to obtain T' (only the node knows the mapping from S -> S') and finally sends T' to the next node on the fluff path (which will again do the same).

So the passing of txs would look like:

Node1(T) ----T'---> Node2(T') ----T''---> Node3(T'') ----T'''---> Nf(T''') ----T''''---> broadcast

So only Node1 knows mapping from T -> T', only Node2 knows mapping from T' -> T'' etc.
When T'''' is broadcasted, nobody knows how they map to the original outputs that have been created (except the owners).

@pkariz
Copy link

pkariz commented Oct 12, 2020

The RFC has this equation

  1. Verify that sG = R + eX, where e is the hash challenge of the i'th kernel.

which has no blinding factors, only receiver's excess X.

Does X here contain the receiver's offset? If yes then swapping should work

You missed my point. Aggregation makes it much less useful for counterparty to know your output.

They still know when you spend these outputs so aggregation only makes it harder to see to who you sent it.

Some potential improvements on the RFC itself:

  • point out that s in (s, i) is receiver's signature
  • in the Verifying Proofs section it's unclear to me how the proof is actually done. Is it done in a way that you show such (sr, receiver_signature) combination that s*G - sr*G = sender.public_nonce + e*sender.public_excess where s is the final kernel signature and (sender.public_nonce, sender.public_excess) are provided by the sender? I would expand the explanation so that it's more detailed and therefore easier to understand
  • i think it would be good if RFC contained an actual example (eg. who sends who what) to see where the timestamp is created etc, it would be easier to follow

Is the proof for sender-initiated transaction the same (flow)? It feels like it could be

- receiver public excess
- `sender_address`

The witness is a triple (s,i,C,m) where i is the MMR index of an on-chain kernel K with commitment C,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo, quadruple not triple

@lehnberg lehnberg added the wallet dev Related to wallet dev team label Oct 12, 2020
@tromp tromp marked this pull request as draft October 13, 2020 21:29
@phyro
Copy link
Member

phyro commented Jun 27, 2021

I believe https://forum.grin.mw/t/eliminating-finalize-step/7621/106 describes another way to produce payment proofs that are the same for both SRS/RSR flow. One of the differences is that the s part of the Schnorr signature is not used as a witness - it instead uses R. While it is questionable whether we'd ever want to do Schnorr half-aggregation (which aggregates the s parts of the signature) because it may make the adaptor signatures impossible in their current form, it might still be worth checking if we can leave this option open.

@tromp
Copy link
Contributor Author

tromp commented Jun 28, 2021

it instead uses R

It uses the set of all participant's R_i, which takes more space. And it uses a nonstandard way of commiting to extra data (R * Hash instead of R + Hash * G as used in Grin switch commitments and Bitcoin taproot). Otherwise it's quite similar to my proof type SenderNonce.

There is something to be said for reducing the number of proof types, but I'm not sure whether we should stop supporting the existing payment proofs.

@phyro
Copy link
Member

phyro commented Jun 28, 2021

It uses the set of all participant's R_i, which takes more space. And it uses a nonstandard way of commiting to extra data (R * Hash instead of R + Hash * G as used in Grin switch commitments and Bitcoin taproot). Otherwise it's quite similar to my proof type SenderNonce.

Indeed, it is a similar strategy. One difference big is what it commits to. The payment proof defined in the forum post requires every participant of a transaction to know all the main ingredients of a transaction that were contributed by other participants (leaving out inputs and outputs). The payment proof is the same for every participant and reveals all sends/receives of a tx (in case there were multiple). It seems like there are different kinds of payment proofs because this one is a bit different compared to other approaches which only constructed the proof for the sender. It's a proof that is the same and "open" to all the participants of a tx regardless of how many there are.

There is something to be said for reducing the number of proof types, but I'm not sure whether we should stop supporting the existing payment proofs.

We will always need to have a fallback for old payment proofs because if the receiver's wallet is on an old wallet version, there's no other way to do it. However, I do believe that we should default to newer types of proofs if we find them superior e.g. if two wallets support a symmetric payment proofs, it (imo) makes sense to use those.

@phyro
Copy link
Member

phyro commented Aug 7, 2021

I wonder if it would make sense having the invoice fields blinded by default. A payment proof can be thought of as an invoice proof and if it has a signature for the following fields

kernel_commitment | amount | timestamp | memo | address

we can instead sign the following

kernel_commitment | H(amount) | H(timestamp) | H(memo) | H(address)

This allows us to show the kernel_commitment and then selectively show other invoice fields. If we don't want to show the amount we simply show the following information kernel_commitment, H(amount), timestamp, memo, address. So hashing serves as blinding of the invoice fields and for each field we can either show the unblinded (unhashed from which we can get hashed version and check the signature) or blinded (already hashed) version. I've not yet thought about the cases this opens (though I've used this in this version of payment proofs which is not how we do payment proofs today). But there may be more cases.

Edit: of course naively doing H(amount) would be bad as one could bruteforce the value. This could be solved by adding a salt to it e.g. have a format <salt>:<field> which for amount could be 5fd924625f6ab16a19cc9807c7c506ae1813490e4ba675f843d5a10e0baacdb8:12.

@tromp
Copy link
Contributor Author

tromp commented Aug 8, 2021

Seems like a complication that will go unneeded in the vast majority of use cases.
Not only the amount, but the timestamp could also be brute forced.
Hashing the memo makes sense to allow for arbitrarily sized memos, that we don't want to include in the slatepack, but communicate separately.

@phyro
Copy link
Member

phyro commented Aug 8, 2021

I agree, this is definitely not needed right now and if at any point we found a use case for it, we could always make a new 'version' of a payment proof.

There is no need for this proof type in SRS flow, as the simpler Invoice type suffices.

The witness is a quintuple (s,i,C,Rs',m) where i is the MMR index of an on-chain kernel K with commitment C, satisfying s\*G = R + e\*X, where R is the `receiver_public_nonce`, X is the `receiver_public_excess`, and e is the hash challenge of kernel K.
and e is the hash challenge of kernel K.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the "and e is the hash..." is repeated twice.

@phyro
Copy link
Member

phyro commented Apr 18, 2022

The discussion here made me think a bit about payment proofs in multiparty setting. I think we can have a uniform and relatively simple construction where we kill two birds with one stone.

In a setting with > 2 parties, we have to randomly scale the contributed pubkeys to solve the problems mentioned in the document (and likely other multisig key canceling problems which is probably why musig2 also scales keys this way). This makes our total excess a linear combination of random scalings of a sequence of pairs (partial_excess, nonce). Slightly changing the sequence of these pubkey pairs would drastically change the E' meaning E' commits to this sequence.

Similarly like you describe in Proof type Invoice, we can make every party sign a message with their grin address containing their part partial_excess || nonce || timestamp || memo. Additionally, receivers must also add amount || sender_addr to the message. These signatures are added to the slate as a participant data along with partial_excess and nonce. Having such signatures around solves 2 things at once. First, they authenticate the (partial_excess, nonce) pair which solves the authentication problem described in the linked document and second, they can act as payment proofs.
To show this, consider a sender that has a signature from the receiver on message receiver_partial_excess || receiver_nonce || timestamp || memo || amount || sender_addr. The sender can show they have a sequence of (partial_excess, nonce) pairs

[(partial_excess1, nonce1), (partial_excess2, nonce2), ... (partial_excessN, nonceN)]

which when through the process of random scaling as described here in the document yield a scaled total excess E'. If a kernel with excess E' is on chain, then this proves the receiver has been paid by the sender for the amount specified in the signature. We now have a (hopefully) secure multiparty slate building with payment proofs without introducing any communication overhead (the process is still linear setupA -> setupB -> setupC -> signC -> signB -> signA) as well as having built in protection against authentication attacks or key canceling. Moreover, I think this works the same for any number of parties 1, 2, 3, ... N which makes it a uniform construction. Note that the payment proof alone doesn't show all parties involved because you only show the sequence of (partial_excess ,nonce) pairs and not all the signatures.

@phyro
Copy link
Member

phyro commented May 2, 2022

@tromp I'm reading Proof type Invoice and want to check my understanding. The steps for the invoice flow are:

  1. The receiver signs the mentioned data and sends (sig, msg) to the sender where sig is the signature and msg is the message they signed
  2. The sender at step2 produces their own partial signature for the kernel with commitment C, saves it locally along with the commitment((ss, Rs), C) and sends the slate for finalization to the receiver
  3. When the sender wants to prove payment, they find a kernel with commitment C on chain and compute sr = s - ss to obtain the missing value for the receiver partial signature equation. This gives the sender all the variables needed to show a valid receiver partial signature (which seems to be our payment proof in this scheme).

This is safe from the sender computing the sr once the transaction hits the mempool and RBF-ing the transaction, because if they were to RBF it, they wouldn't be able to point to the kernel on the chain, correct?

@tromp
Copy link
Contributor Author

tromp commented May 3, 2022

Correct; payment proofs require an on-chain kernel, and a convincing proof requires that kernel to have sufficient confirmation.
While in RSR flow, the sender doesn't see the receiver partial signature until it hits the mempool, in SRS flow the sender sees it before it finalizes.

@phyro
Copy link
Member

phyro commented May 3, 2022

Regarding the implementation of this, would it make sense to define the payment proof as <M, sig(M, receiver_addr), (ss, Rs), C> where M is the message the receiver signs for the payment proof, sig(M, receiver_addr) is the signature for it, and then we have the partial signature of the sender and the kernel commitment? I think this would make for a unified payment proof structure for both flows while also making sure the sender gets all the payment proof data at the moment they sign a transaction because they have this information available both in RSR at step2 as well as in SRS at step3. Instead of computing the partial signature of the receiver when the transaction gets to the mempool or is observed on the chain, the sender does this lazily only when they want to prove the payment proof.
So given a payment proof <M, sig(M, receiver_addr), (ss, Rs), C>, the verifier:

  1. verifies sig(M, receiver_addr) is valid
  2. finds the kernel with commitment C and subtracts the sender's partial signature from the kernel verification equation R - Rs + e*X - e*Xs == s - ss*G which leaves them only with the equation for the receiver's partial signature Rr + e*Xr == sr*G
  3. verification of the payment proof is then swapping the left side of the equation with receiver values (partial excess, nonce) from M and checking the formula is still balanced

While this does add another curve point to the payment proof, it does away with edge-cases e.g. requiring keeping the sender partial sig around in RSR flow until the transaction hits the chain (and removing it later) and potentially modifying the payment proof structure when we observe the kernel on the chain. I think it's simpler to implement and reason about this if the sender gains a complete payment proof data the moment they sign the transaction. Then, they just have to wait for the kernel to appear on the chain to obtain the mmr index of the kernel (which is only used for faster proving, rather than computing the proof itself).

Of course, we would have an optional field i for the kernel index which the wallet could be populating later on when the kernel arrives on the chain, but it's much simpler to just set an optional value than to reason about the local state (which flow we have) and implement branching logic about what needs to be updated and what we can forget about.

@tromp
Copy link
Contributor Author

tromp commented May 3, 2022

R - Rs + eX - eXs == s - ss*G

You should write (X - Xr) instead of Xs?!

it does away with edge-cases e.g. requiring keeping the sender partial sig around

Keeping context data around is already the task of a wallet slate context; it's not an edge case.

if the sender gains a complete payment proof data the moment they sign the transaction.

A payment proof is not complete without an on-chain kernel. I don't think we should leave the locating of a kernel to the verifier. As in PoW, verification effort should be minimized.

@phyro
Copy link
Member

phyro commented May 3, 2022

You should write (X - Xr) instead of Xs?!

No, I meant Xs because in the definition I made above we remember the sender partial signature which is then subtracted from the full kernel signature equation (which leaves us with the signature equation of the receiver partial sig). The reason I want to save the sender partial signature instead of parts of the receiver partial signature is because this one is always available when the sender signs which gives you all the data needed to verify the proof (except for the kernel mmr index).

Keeping context data around is already the task of a wallet slate context; it's not an edge case.

What I meant with an edge case was some implementation details that came to mind one being that I think we delete the transaction context once we sign (if I recall correctly, that's true for the current implementation as well). When I was doing the contract implementation, I made sure we delete it to avoid any potential double-signing (even though we could in theory just forget the secret keys, but that's not how it works today). I'll take some time to check how exactly we do today's payment proofs and where the data is stored.

A payment proof is not complete without an on-chain kernel. I don't think we should leave the locating of a kernel to the verifier. As in PoW, verification effort should be minimized.

Fair point, sounds reasonable.

@tromp
Copy link
Contributor Author

tromp commented May 3, 2022

Xs is not specified in your suggested payment proof of <M, sig(M, receiver_addr), (ss, Rs), C> though, while X=C and Xr (as part of M) are?!

the receiver partial signature is because this one is always available when the sender signs

We only need receiver partial signature to be available once the kernel is confirmed on-chain.

we delete the transaction context once we sign

The wallet always needs to keep a record of pending transactions and preferably past transactions as well (at least recent ones). Such records would include all payment proof related data.

@phyro
Copy link
Member

phyro commented May 3, 2022

Xs is not specified in your suggested payment proof

I'll escort myself out. You're right of course, this would need to be a part of the payment proof as well.

To accomodate the various proof types, the slate will include the following related fields:

* `receiver_address` - An ed25519 public key for the receiver, typically the public key of the user's v3 onion address.
* `timestamp` - The time at which the receiver generates the payment promise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not urgent, I'm just considering an experimental implementation of this now to correspond with unified transaction flow.

timestamp should be unambiguously defined here, assuming it's epoch time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the same 64-bit timestamps appearing in Grin headers.


* `receiver_address` - An ed25519 public key for the receiver, typically the public key of the user's v3 onion address.
* `timestamp` - The time at which the receiver generates the payment promise
* `memo` - A string of size at most 32 bytes that may contain additional payment details,
Copy link
Member

@yeastplume yeastplume Mar 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the intent here to have this field arbitrarily sized (which has implications for binary serialization, which we support)? It might be more clear to define this as 'an optional 32 bytes of data that may contain, serialized in string-hex representation'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think for definiteness we should allow any memo up to say 1KB, and make this field the mandatory memohash (blake2b of memo).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that makes sense and covers future possibilities a bit better. I'll draw your attention to the Slate V5 PR once it's ready for review, and we can revise this RFC based on that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wallet dev Related to wallet dev team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants