owned this note
owned this note
Published
Linked with GitHub
# Scaled specs
## StateBLS Contract
1. Receipt is defined as folowing
```!
Receipt {
// a's index
aIndex (8 bytes)
// b's index
bIndex (8 bytes)
// amount that b owes a
amount (128 bytes)
// timestamp after which this
// receipt is no more valid
expiresBy (4 bytes)
// no. of receipt
seqNo (2 bytes)
}
```
A Receipt is always unidirectional (i.e. b can only owe a amount, not vice versa). Receipt is considered valid if both `a` and `b` have signed it. `a` and `b` exchange receipt off-chain for `b` to pay `a`.
The contract defines a weekly (approx) cycle and a receipt can choose to expire in any of the cycles in future. Thus, `expiresBy` is always some multiple of 604800 (seconds in a week). This allows us to derive `expiresBy` on-chain saving us calldata cost.
`seqNo` describes the sequence of the receipt. It allows a & b to settle their current receipt on-chain, settle the balance and subsequently start with new receipt with `seqNo + 1`. Note that `a` should make sure that last receipt with `seqNo = n` is settled properly before starting a new receipt with `seqNo = n + 1`.
During a particular `seqNo` - `b` can pay `a` any number of times off-chain by increasing the `amount` on the receipt for each subsequent payment. Note that `amount` can only increase in every subsequent update, otherwise `a` risks fraud by `b`.
2. User
A `user` is assigned an index upon registration and should be always referred with their index. To register user provides (1) address (2) bls pubkey (uint256[4]). User can update (1) anytime, but not (2).
User is expected to maintain sufficent security deposit so that others that are willing to accept payments from them using `receipts`.
User risks losing their entire security deposit if they spend more than their account balance. Note that anyone can cause to slash their security deposit by submitting a valid receipt that causes user's balance to go below 0.
If user is accepting payment from someone (i.e. they are `a` and the other party is `b`) they should make sure that `b` has sufficeint balance + relatively high security deposit (2x OR 3x the `amount` in receipt).
3. Security Deposit
Every user is expected to submit certain amount as security deposit in the contract. It is needed to deter users from overspending (i.e. having receipts with others which when aggregated exceeds user's account balance). If a user overspends (i.e. a valid receipt was submitted on-chain that causes user's balance to go -ve) their entire deposit will be slashed.
Slashing security deposit to deter overspending is only effective if the upside that user gains from overspending is bounded + less than their deposit. Thus user `a` should always reject receipts with `amount` from user `b` where `b's security deposit` < (`2 * amount` || `3 * amount`) (will update exact values in future) OR `b's account balance` is running low relative to `amount`.
4. Withdrawal
Withdrawls are processed in two steps. Let's say user `b` wants to withdraw `amount` from the account. They first call `initWithdrawl(bIndex, amount)` to indicate their intention of withdrawing `amount`. Then after a fixed buffer period they call `processWithdrawal(bIndex)` to finish the withdrawal process.
The fixed buffer period is needed to allow for other users with which user `b` shares receipts and owes amount can post their shared receipts with `b` on-chain to settle.
5. Receipts settlement
`a` collects latest receipts that it shares with several others (many `b`s) and settles them on-chain at once using `post()` function.
Note that at present we differentiate between `a` and `b`. `a` is someone that provides some service for which they get paid in microtransactions. So it's suitable for `a` to aggregate all receipts that they have receivd and post them all at once. `b` is someone that usually uses different services for which they pay in micro-transactions.
Since the latest receipt that `a` shares with some `b` with `seqNo` always has the greatest amount, it is expected of `a` to always use the latest receipt when calling `post()`. Thus in `post()` we don't guarantee that receipt posted is the latest receipt, rather we expect `a` to do so.
To limit the ability for `post()` fn to be called by only `a` (i.e. all receipts in calldata have `aIndex` set to index of `a`) ~ `a` is expected to commit to fn call by signing on the commitment data. `commitmentData` is as follows -
```!
bytes memory commitmentData;
for r in receipts {
bytes.concat(
commitmentData,
r.bIndex,
r.amount,
r.seqNo
)
}
```
where
`receipts` are all latest receipts that `a` shares with others that `a` wants to settle on-chain.
Since in a valid receipt the combination of `bIndex` and `seqNo` would always be unique, replay attacks are not possible. Thus we don't need any additional data for making `commitmentData` unique for every `post()` fn call.
## Open questions
1. We try to bound the upside of double spending by limiting how much amount a user can specify in a receipt relative to their security deposit.
But someone can do the following - Have a security deposit of 10 USD. Have 50 receipts with 0.5 USD. The amount in each receipt is low relative to security deposit, so others won't deny from accepting. But in reality user has overcommited.
One way to tackle this is to redeem receipts on-chain at higher frequency. But this would be bad for users that exchange receipts less often
Another way to tackle such situations is to have a way for the `a` to have latest view on receipts that `b` shares with others. We can do this by having every `a` (i.e. the ones that receive payments) publish receipts on a p2p network, thru which we can form view of latest receipts committed by every participant (note that publishsing received receipts is in interest of all `a`s to do so).
We don't require every user to store latest view on all receipts and listen to p2p protocol, rather we can have few providers that would do this and anyone can query them for info (we can support such providers thru GitCoin or some other mechanism)
2. Can we reduce the calldata size per update further (for ex. using ZKP)?
Right now calldata size per update is 24 bytes {bIndex: 8 bytes; amount: 16 bytes}
We can replace the entire system with SMT that maintains account objects (Account Tree) on-chain of eash user. To update on-chain account tree ~ user would take their receipts and generate a proof that proves the following - (1) All receipts have valid signatures and they in aggregation hash to "H" (2) When account tree is updated to reflect amounts in receipts the updated account tree state root is "R". User then post "H" & "R" on-chain along with proof to update Account tree. More about Optmisitc Version of this is [here](https://hackmd.io/d38J9USRTmO0gVBmxKBbhw).
One drawback of this is that there's no way for others to know what Account tree looks like after the update. For this we need user to post atleast the changes in each account in the update. This will increase the calldata size ~ thus making it equivalent to the BLS approach. IMO BLS approach is lighter & simpler than this.
This also does not eliminates the challenge period ~ since we don't have any way to prove that the receipt that was included in the update was the latest receipt agreed by `a` & `b`. We still need a time period during which the update can be challenged, if receipt used wasn't the latest one.
3. We can reduce amount size (i.e. uint128) by denominating`amount` in some other unit. For example ~ considering amount value 1 as 1 cent, so the smallet value can only be 1 cent. This way we don't need uint128 to represent amount in receipts, rather we can use uint64 or even smaller.
<!-- ## Normal posting
// extra
If `commitmentData` included only `postNonce + 1`, then it would be possible to front-run transaction published by `a` containing commit to `commitmentData` and latest receipts and replace latest receipts with old receipts.
their latest `postNone + 1` when calling `post()`. `postNonce` tracks number of times `a` has called `post()`. Since `postNonce` increments by 1 after every succesful `post()` fn call by `a`, it prevents replay attacks (i.e. no one can pretend to be `a`). In absence of `postNonce` (i.e. no signature) following attack would be possible -
Consider `b` shares a receipt with `seqNo` with `a` according to whcih `b` owes `x` amount to `a`. However, there exist an old valid receipt between `b` & `a` with same `seqNo` as latest receipt but with amount < `x`. Now if we don't make sure that only `a` calls `post()` fn then `b` can call `post()` fn with old receipt pretending to be `a`. Since `post()` does not guarantee the receipt as latest, the receipt will be accepted. Thus, the latest receipt with `seqNo` would be rendered invalid and `a` would lose money.
```!
{
bIndex (8 bytes),
amount (16 bytes),
expiresBy (4 bytes)
aSignature (65 bytes)
bSignature (65 bytes)
}.
1. We are deriving seqNo. for receipts on-chain
2. bIndex is unique id assinged to `b` that stores their address.
3. We don't include aIndex in every receipt, rather we include it once in the calldata.
4. TODO - Make expiresBy deterministic in later versions
```
Compressing updates[]: Calldata should have array of updates (i.e. bytes) compressed in the format -
```
{
bAddress (20 bytes),
amount (16 bytes),
seqNo (2 bytes),
expiresBy (4 bytes)
aSignature (65 bytes)
bSignature (65 bytes)
}.
```
We use common `aAddress` for every update.
That means every update has 172 bytes of non-zero data (I am assuming no 0 bytes), thus l1 data gas units / update = 2752 units.
In case of optimism l1 data cost = l1_gas_price * ((2752 * no_of_updates) + 2100) * 1.25
-->
<!--
## Points for compressing data fruther
1. remove `expiresBy` and replace with a proof that all receipts have `expiresBy` greater than some `timestampA` that is greater than `block.timestamp` on chain (You don't need this anymore ~ use cycles instead).
2. remove signatures and generate proof that all receipts have valid signatures (Use BLS for now ~ ZK is complicated and intensive for this task).
-->