Meridian: Off-chain scheduled rewards
| Created by | |
|---|---|
| Created time | |
| Last edited by | |
| Last edited time | |
| Tags |
Design
Motivation
The current L1 smart contract based system is
- too expensive, when the L1 is expensive
- too slow, when the L1 is expensive and messages get stuck
Goals
- Gas costs are minimal. Instead of updating scores per participant every round, do it less frequently, once per payment period, or not at all.
- All state should still be publicly readable
- Calls are authenticated with signatures / public key cryptography
- Users don’t notice a difference
- Rewards should come at the same frequency
- Rewards should come from the same address
- Scheduled rewards can be read as before
- Don’t deploy a new version of the smart contract
Assumptions
Since the new service needs to keep track of balances across rounds, and doesn’t necessarily know how much FIL is still available (the FIL won’t live in the smart contract), assume that always the full per-round FIL amount is available. One round equals 0.456621004566210045 FIL. This way the logic of converting scores to FIL is trivial: reward = score / MAX_SCORE * 0.45.. FIL.
Limitations
There are drawbacks of all these designs, without which off-chain scheduled rewards aren’t possible:
➖ Per-round scores aren’t stored in the smart contract
➖ Because per-round scores aren’t stored in the contract, you can’t put future rewards into the smart contract. You can only send funds to the smart contract for work that has already been done (through addBalances()). This means that whoever holds the funds before retroactively sending rewards to the contract, will hold other peoples’ money.
We can limit this risk by transferring rewards to the contract say once per day, but still, for that one day, we will hold other peoples’ money. If we for example spend it (for whatever reason), then they will receive nothing at the end of the day, despite having work done.
It is possible to bypass this limitation by changing the Meridian smart contract:
- Station admins send funds to the smart contract
- At the end of a payout period, instead of calling
addBalances()(which requires funds to be sent), call a new method, egassignBalances(), which assigns parts of the funds living in the smart contract to the participants.
Non-Goals
- Decentralize the new service. Only the Station team can run the new
spark-rewardservice. If another team wants to run it, they won’t be able to host it at the address thatspark-evaluateexcepts to submit scores to. This is making the whole system a bit less trustless, as before, this state was kept in a smart contract. On the other hand, we don’t yet have the primitives for decentralizing this another way.spark-evaluateworks the same way.
Revisions
Rev A “Trust a web service”
Goal: Intuitive first version of the design
Introduce a new service spark-rewards, which will keep all scores for the current payment period (7d). It also keeps the funds. Admins can tell it to release rewards to participants. Effectively spark-rewards takes over the scores and rewards part of the smart contract, which is now only used for sharing measurements.
sequenceDiagram
participant a as Station admins
participant e as spark-evaluate
participant r as spark-rewards
participant p as Station participants
loop Increase scores
e->>r: POST /scores w/sig
r->>r: Increase balances
end
loop Release rewards
a->>r: Send FIL
a->>r: POST /release w/sig
r->>p: Send FIL
r->>r: Clear balances
end
p->>r: GET /scheduled-rewards/:participant➖ Funds live in the spark-rewards service wallet, not the smart contract
➖ Station participants have to trust spark-rewards to distribute rewards properly
➖ Until FIL is released (every 7d), it will be held either in Station admin wallets, or the spark-rewards wallet. Both don’t want to hold funds that belong to other people.
Rev B “Trust a CLI”
Goal: Instead of the service sending FIL, do it from a Ledger
Remove some authority by keeping funds in Station admins’ account, not the spark-rewards. Rewards are distributed by a local script that Station admins run. spark-rewards merely acts as the scores database now.
sequenceDiagram
participant e as spark-evaluate
participant r as spark-rewards
participant a as Station admins
participant p as Station participants
loop Increase scores
e->> r: POST /scores w/sig
r->>r: Increase balance
end
loop Release rewards
a->>r: GET /scores
a->>p: Send FIL
a->>r: POST /paid-rewards?participant&amount&address w/sig
r->>r: Clear balances
end
p->>r: GET /scheduled-rewards/:address➕ Trustless: Anyone can GET /scores , send FIL and then call POST /paid-rewards (might have legal problems)
➖ Funds live in an admin wallet, not the smart contract
➖ Station admins need to run a script that gets scores, sends FIL and marks FIL as sent
➖ Station admins hold the FIL that will later be distributed (every 7d). We don’t want to hold other peoples’ money.
Rev C “Trust a web service to be admin on the smart contract”
Goal: Distribute funds through the smart contract.
Instead of bypassing the smart contract’s scores and reward distribution logic, only bypass the scores logic. spark-rewards keeps track of scores, then calls addBalances() on the contract (which also sends FIL), and then releaseRewards(). Starting with this revision, Station participants now won’t notice any difference to the current setup.
sequenceDiagram
participant a as Station admins
participant e as spark-evaluate
participant r as spark-rewards
participant c as Meridian IE
participant p as Station participants
loop Increase scores
e->>r: POST /scores w/sig
r->>r: Increase balances
end
loop Sync rewards
a->>r: POST /publish-rewards w/sig
a->>r: Send FIL
r->>c: addBalances() (send FIL)
r->>r: Clear balances
end
loop Release rewards
r->>c: releaseRewards()
c->>p: Send FIL
end
p->>r: GET /scheduled-rewards/:address➕ spark-rewards only holds temporary scores, until flushing them to the contract with addBalances()
➕ The contract is again responsible for distributing the rewards
➕ Station admins don’t need to run a potentially complex / buggy CLI, it’s only one HTTP call to publish rewards
➕ Aligned with spark points product perspective
➖ Dangerous: The nodejs service has admin privileges on the smart contract, otherwise it couldn’t call these two methods
➖ Also dangerous is that until the end of the payment period, either Station admins or the service account owns the FIL. We don’t want either to hold other peoples’ money.
👉 Winner: Rev D 👈
Goal: Instead of spark-rewards calling addBalances() and releaseRewards() , let Station admins perform these. Don’t give spark-rewards admin privileges on the smart contract.
spark-rewards is the scores database, and Station admins use a local CLI for periodically (once per week) reading these scores and adding them to the balance in the smart contract. The smart contract distributes the rewards. Once this is done, the CLI updates state in spark-rewards.
sequenceDiagram
participant a as Station admins
participant e as spark-evaluate
participant r as spark-rewards
participant c as Meridian IE
participant p as Station participants
loop Increase scores
e->>r: POST /scores {addresses,scores} w/sig
r->>r: Increase balances
end
loop Sync rewards
a->>r: GET /scores
a->>c: addBalances() (send FIL)
a->>r: POST /added-balances {addresses,balances} w/sig
r->>r: Clear balances
end
loop Release rewards
a->>c: releaseRewards()
c->>p: Send FIL
end
p->>r: GET /scheduled-rewards/:address➕ Doesn’t give spark-rewards admin privileges on the smart contract
➖ Station admins (or their scripts) have to be trusted to keep balances in the smart contract and the spark-rewards db in sync
➖ Station admins hold the funds for the whole payment period (7d)
Rev E “Trust an admin a bit less”
Goal: Make spark-evaluate validate that balances have been added, so Station admins don’t need to update that state.
spark-rewards is the scores database, and Station admins use a local CLI for periodically (once per week) reading these scores and adding them to the balance in the smart contract. The smart contract distributes the rewards. spark-rewards will itself update its scores state after seeing on the smart contract that balances have been added.
sequenceDiagram
participant a as Station admins
participant e as spark-evaluate
participant r as spark-rewards
participant c as Meridian IE
participant p as Station participants
loop Increase scores
e->>r: POST /scores w/sig
r->>r: Increase balances
end
loop Sync rewards
a->>r: GET /scores
a->>c: addBalances() (send FIL)
r->>c: getBalances()
r->>r: Clear balances
end
loop Release rewards
a->>c: releaseRewards()
c->>p: Send FIL
end
p->>r: GET /scheduled-rewards/:address➕ spark-evaluate balances state is automatically kept in sync with the smart contract
➕ The CLI that Station admins run is simpler now
➖ For the duration of the payout period (7d), Station admins will hold other peoples’ money.
➖ It’s impossible for spark-rewards to reliably read from the smart contract how it should adjust its internal scores
Rev F “Only transactions are real”
Goal: Let spark-rewards reliably reconcile scores with chain state
spark-rewards is the scores database, and Station admins use a local CLI for periodically (once per week) reading these scores and adding them to the balance in the smart contract. The smart contract distributes the rewards. spark-rewards will itself update its scores state after validating the transaction hashes the CLI shared with it.
sequenceDiagram
participant a as Station admins
participant e as spark-evaluate
participant r as spark-rewards
participant c as Meridian IE
participant x as Block Explorer
participant p as Station participants
loop Increase scores
e->>r: POST /scores w/sig
r->>r: Increase balances
end
loop Sync rewards
a->>r: GET /scores
a->>c: addBalances() (send FIL)
a->>r: POST /validate-transaction/:hash
r->>x: Get balance changes
r->>r: Update balances
end
loop Release rewards
a->>c: releaseRewards()
c->>p: Send FIL
end
p->>r: GET /scheduled-rewards/:address➕ Internal station-rewards balance state is updated by reading from the blockchain
➖ At least one known and integrated block explorer needs to be online
➖ If the admin doesn’t call POST /validate-transaction/:hash, spark-rewards will get out of sync with the smart contract
Rev G “Lean on block explorers”
Goal: Let spark-rewards discover transactions through a block explorer, so that admins don’t need to tell about them
spark-rewards is the scores database, and Station admins use a local CLI for periodically (once per week) reading these scores and adding them to the balance in the smart contract. The smart contract distributes the rewards. spark-rewards will itself update its scores state after validating the transaction hashes discovered through a block explorer.
sequenceDiagram
participant a as Station admins
participant e as spark-evaluate
participant r as spark-rewards
participant c as Meridian IE
participant x as Block Explorer
participant p as Station participants
loop Increase scores
e->>r: POST /scores w/sig
r->>r: Increase balances
end
loop Sync rewards
a->>r: GET /scores
r->>r: Call "Sync rewards with Chain"
r->>a: Return scores
a->>c: addBalances() (send FIL)
end
loop Sync rewards with Chain
r->>x: Get smart contract transactions
r->>r: Parse `addBalances()` arguments
r->>r: Update balances
end
loop Release rewards
a->>c: releaseRewards()
c->>p: Send FIL
end
p->>r: GET /scheduled-rewards/:address➕ Internal station-rewards balance state is updated by reading from the blockchain
➕ Admins / the CLI can’t possibly forget about telling station-rewards about a balance update
➖ At least one known and integrated block explorer needs to be online
➖ ⚠️ How can read after write consistency be guaranteed?
- The problem is finality (F3), to have high certainty you should wait 900 blocks
Future goals
- Since
addBalances()is an admin method on the smart contract, still only the trusted Station admins can perform this step. Remove this requirement.