Loopring / protocols

A zkRollup DEX & Payment Protocol
https://loopring.org
329 stars 122 forks source link

[Loopring 3.6] Add addition field to Order to enable open/closed matching #1422

Closed dong77 closed 4 years ago

dong77 commented 4 years ago

The reason we added Dual Authoring (DA) is to support the following two use cases:

DA will make sure the sender can create open transfer requests that don't specify the to fields. The receiver can fill in the to fields and signs the modified transfer request by not sharing the DA key with our relayer, making sure the relayer won't be able to become the receiver to steal the money.

In China, the most popular payment apps are WeChat and Alipay. In many cases, vendors will scan consumer's QR code -- the consumer will simply show the QR code without specifying the amount and he doesn't care about the vendor's account. According to hoss, this payee-scan-payer scenario are most people where the payer doesn't have access to the internet.

Challenges & Suggestion

The problem with the transfer-based red envelope solution is that if a transfer with a bigger nonce is included in a block, pending open transfers will fail. After a discussion in Shanghai, we realized we have done this red-envelop thing sometime ago, by using trades, not transfers. We believe it's more elegant as it doesn't use nonce at all. Here is how:

If we implement DA for trades, we can make this process also entered trustless, but I feel it's an overkill. I don't think we want to pay the price of making the spot-trade circuit bigger simply because of such use cases. Remember REs are mostly used among friends, coworkers, and relatives, and the amount is small.

The true value of DA for trades is to enable trustless OTC (peer-to-peer) trades without trusting the relayer. But again, I don't think we want to complicate the trade circuits for that.

The payee-scan-payer can also be implemented with an open-trade. The payer create an order at price 0. and the payee takes the order by paying 0.

So here are my suggestions:

We remove DA from transfers to simplify the corresponding circuits. If we like DA, we should implement it for trades, not transfers.

Brechtpd commented 4 years ago

Usually, each envelope contains 0.1-10 USD. The sender will create a RE and share in WeChat (that's the only action he needs to do), people in the group will be able to click on the RE to claim his share, a random percentage of the total amount.

Ah, I didn't know this was also possible. That does make it more trade like, this would need multiple transfers signed by the payer for each max amount transferred.

If we implement DA for trades, we can make this process also entered trustless, but I feel it's an overkill. I don't think we want to pay the price of making the spot-trade circuit bigger simply because of such use cases. Remember REs are mostly used among friends, coworkers, and relatives, and the amount is small.

This would indeed increase costs for trades a bit, but does not for transfers (both need to check up to 2 signatures). I guess it would be acceptable for small amounts, but not really payments. It does make it impossible to decentralize the operators in some respect, in that case you can't just trust a single party so could be problematic for those reasons.

The true value of DA for trades is to enable trustless OTC (peer-to-peer) trades without trusting the relayer. But again, I don't think we want to complicate the trade circuits for that.

If the counter party is known, this can also be implemented by having an optional taker field in the order, so the order can only be used by orders of that single address. In terms of logical complexity DA is actually also super easy to implement because you just hash some extra data and check another signature and you're done. Personally I'm much more worried about adding special logic and introducing special cases we have to watch out for.

The possible problems I can currently think of by implementing this with trades:

I think it would be possible to do it like this, but seems like it will definitely needs some changes to the trading logic.

And with the way I understand REs now I would agree that something trade like is a better fit than transfers. But like I said before, there is no reason to push this functionality in any of the existing transaction types if it doesn't make sense! If it needs special logic, it needs dedicated code and it makes sense to have on it's own. I like to think about it like this: if you would write the code for it in a smart contract, how would you design it? If the answer is a different function to process the transaction than I believe we need to have a dedicated sub-circuit for it as well. But if the trade circuit only needs very minimal changes than great, but almost none of the trading functionality will actually be necessary. Otherwise a special transfer with trade like trading history behaviour could make more sense.

Maybe it still makes sense to keep DA for transfers though... It doesn't really significantly inrease logical complexity and it does not increase the number of constraints. And I guess it would also be kind of hard to explain if this system would be used for payments that this isn't really non-custodial I guess... Of course, if it won't be used then it doesn't make sense to keep it. And could even by completely unnecessary depending on how the REs end up being implemented.

The problem with the transfer-based red envelope solution is that if a transfer with a bigger nonce is included in a block, pending open transfers will fail.

After a discussion in Shanghai, we realized we have done this red-envelop thing sometime ago, by using trades, not transfers. We believe it's more elegant as it doesn't use nonce at all.

This is impossible currently because of the strict nonce+1 rules. But it seems like this nonce problem keeps popping up and it can limit functionality in multiple ways so I believe we should improve it regardless how we implement the REs. An improvement that's beneficial for all transactions that use nonces!

We could just reuse our orderID system, which I think may actually be best. It's already there and being used any way, so don't see a lot of drawbacks really. We can also slightly modify our current nonces. Our nonce is currently 32bit. We could just split those nonce bits up in 2 parts internally (so transactions/Merkle tree still store a single nonce value like now):

Group (16bit, MSB)
Channel (16bit, LSB)

Within a group we enforce nonce.value+1 like we do know, but each channel in a group can be used in parallel and the bit is set once it is used. So we can do up to 16 parallel transactions. Sequential transactions can be done by only doing a single transaction in a group. At any point of time when transactions don't need to be sequential a free channel is picked in the current group, if all bits are used up the group value is increased by 1 and all channel bits are reset. This reduces the maximum number of transactions in an account to 2^16 16 = 1M, which may be a bit low so we might have to increase the number of bits available for a nonce. This only impacts the DA minimally, so no problem really with just using 64bit nonces so we have very high limits. But this basically recreates the orderID system on some rudimentary level, just a bit differently because we only need 1 bit of information here instead of full fill amounts per slot. Seems to make more sense to just have a single system that can do both, which the current orderID system already can. We'd just have to bring the tree to the account level instead of having a separate tree per balance. Maybe even a good thing, and not much of loss now that we support infinite number of orderIDs.

Any way, I'm sure we'll discuss this tomorrow. :)

dong77 commented 4 years ago

Good to read all your thoughts, there are certainly a lot that I haven't considered...

I would suggest the following:

Let's discuss it.

dong77 commented 4 years ago

After a discussion with Brecht, DA will be kept for transfers.

Below is a summary for the conversation:

Brecht, here is a summary of our conversation:

address liquidityProvider=fastWithdrawnals[user][token][nonce][amount];
address withdrawTo = liquidityProvider == address(0)? withdrawal.to : liquidityProvider;
depositContract.withdrawal(,..., withdrawTo)
delete fastWithdrawals[user][token][nonce][amount];
}