feat: partial rewards and withdraws (#880)

* feat: partial rewards and withdraws

* test: missing reserve slot

* test: fix contracts
This commit is contained in:
Adam Uhlíř 2024-10-10 13:53:33 +02:00 committed by Slava
parent b0607d3fdb
commit eff0d8cd18
No known key found for this signature in database
GPG Key ID: 351E7AA9BD0DFEB8
8 changed files with 65 additions and 18 deletions

View File

@ -1,5 +1,6 @@
import pkg/metrics
import ../statemachine
import ../../logutils
import ./error
declareCounter(codex_purchases_failed, "codex purchases failed")
@ -12,5 +13,9 @@ method `$`*(state: PurchaseFailed): string =
method run*(state: PurchaseFailed, machine: Machine): Future[?State] {.async.} =
codex_purchases_failed.inc()
let purchase = Purchase(machine)
warn "Request failed, withdrawing remaining funds", requestId = purchase.requestId
await purchase.market.withdrawFunds(purchase.requestId)
let error = newException(PurchaseError, "Purchase failed")
return some State(PurchaseErrored(error: error))

View File

@ -16,5 +16,7 @@ method `$`*(state: PurchaseFinished): string =
method run*(state: PurchaseFinished, machine: Machine): Future[?State] {.async.} =
codex_purchases_finished.inc()
let purchase = Purchase(machine)
info "Purchase finished", requestId = purchase.requestId
info "Purchase finished, withdrawing remaining funds", requestId = purchase.requestId
await purchase.market.withdrawFunds(purchase.requestId)
purchase.future.complete()

View File

@ -269,7 +269,9 @@ method freeSlot*(market: MockMarket, slotId: SlotId) {.async.} =
method withdrawFunds*(market: MockMarket,
requestId: RequestId) {.async.} =
market.withdrawn.add(requestId)
market.emitRequestCancelled(requestId)
if state =? market.requestState.?[requestId] and state == RequestState.Cancelled:
market.emitRequestCancelled(requestId)
proc setProofRequired*(mock: MockMarket, id: SlotId, required: bool) =
if required:

View File

@ -230,3 +230,21 @@ checksuite "Purchasing state machine":
let next = await future
check !next of PurchaseFinished
test "withdraw funds in PurchaseFinished":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
discard await PurchaseFinished().run(purchase)
check request.id in market.withdrawn
test "withdraw funds in PurchaseFailed":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
discard await PurchaseFailed().run(purchase)
check request.id in market.withdrawn
test "withdraw funds in PurchaseCancelled":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
discard await PurchaseCancelled().run(purchase)
check request.id in market.withdrawn

View File

@ -17,6 +17,10 @@ ethersuite "Marketplace contracts":
var periodicity: Periodicity
var request: StorageRequest
var slotId: SlotId
var filledAt: UInt256
proc expectedPayout(endTimestamp: UInt256): UInt256 =
return (endTimestamp - filledAt) * request.ask.reward
proc switchAccount(account: Signer) =
marketplace = marketplace.connect(account)
@ -46,6 +50,7 @@ ethersuite "Marketplace contracts":
switchAccount(host)
discard await token.approve(marketplace.address, request.ask.collateral)
discard await marketplace.reserveSlot(request.id, 0.u256)
filledAt = await ethProvider.currentTime()
discard await marketplace.fillSlot(request.id, 0.u256, proof)
slotId = request.slotId(0.u256)
@ -87,8 +92,7 @@ ethersuite "Marketplace contracts":
let startBalance = await token.balanceOf(address)
discard await marketplace.freeSlot(slotId)
let endBalance = await token.balanceOf(address)
check endBalance == (startBalance + request.ask.duration * request.ask.reward + request.ask.collateral)
check endBalance == (startBalance + expectedPayout(requestEnd.u256) + request.ask.collateral)
test "can be paid out at the end, specifying reward and collateral recipient":
switchAccount(host)
@ -105,7 +109,7 @@ ethersuite "Marketplace contracts":
let endBalanceCollateral = await token.balanceOf(collateralRecipient)
check endBalanceHost == startBalanceHost
check endBalanceReward == (startBalanceReward + request.ask.duration * request.ask.reward)
check endBalanceReward == (startBalanceReward + expectedPayout(requestEnd.u256))
check endBalanceCollateral == (startBalanceCollateral + request.ask.collateral)
test "cannot mark proofs missing for cancelled request":

View File

@ -20,8 +20,12 @@ ethersuite "On-Chain Market":
var slotIndex: UInt256
var periodicity: Periodicity
var host: Signer
var otherHost: Signer
var hostRewardRecipient: Address
proc expectedPayout(r: StorageRequest, startTimestamp: UInt256, endTimestamp: UInt256): UInt256 =
return (endTimestamp - startTimestamp) * r.ask.reward
proc switchAccount(account: Signer) =
marketplace = marketplace.connect(account)
token = token.connect(account)
@ -42,6 +46,7 @@ ethersuite "On-Chain Market":
request = StorageRequest.example
request.client = accounts[0]
host = ethProvider.getSigner(accounts[1])
otherHost = ethProvider.getSigner(accounts[3])
slotIndex = (request.ask.slots div 2).u256
@ -447,8 +452,11 @@ ethersuite "On-Chain Market":
let address = await host.getAddress()
switchAccount(host)
await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
let filledAt = (await ethProvider.currentTime()) - 1.u256
for slotIndex in 0..<request.ask.slots:
for slotIndex in 1..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
@ -456,13 +464,11 @@ ethersuite "On-Chain Market":
await ethProvider.advanceTimeTo(requestEnd.u256 + 1)
let startBalance = await token.balanceOf(address)
await market.freeSlot(request.slotId(0.u256))
let endBalance = await token.balanceOf(address)
check endBalance == (startBalance +
request.ask.duration * request.ask.reward +
request.ask.collateral)
let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256)
check endBalance == (startBalance + expectedPayout + request.ask.collateral)
test "pays rewards to reward recipient, collateral to host":
market = OnChainMarket.new(marketplace, hostRewardRecipient.some)
@ -471,7 +477,11 @@ ethersuite "On-Chain Market":
await market.requestStorage(request)
switchAccount(host)
for slotIndex in 0..<request.ask.slots:
await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
let filledAt = (await ethProvider.currentTime()) - 1.u256
for slotIndex in 1..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
@ -486,6 +496,6 @@ ethersuite "On-Chain Market":
let endBalanceHost = await token.balanceOf(hostAddress)
let endBalanceReward = await token.balanceOf(hostRewardRecipient)
let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256)
check endBalanceHost == (startBalanceHost + request.ask.collateral)
check endBalanceReward == (startBalanceReward +
request.ask.duration * request.ask.reward)
check endBalanceReward == (startBalanceReward + expectedPayout)

View File

@ -44,7 +44,7 @@ twonodessuite "Marketplace", debug1 = false, debug2 = false:
check reservations.len == 5
check reservations[0].requestId == purchase.requestId
test "node slots gets paid out":
test "node slots gets paid out and rest of tokens are returned to client":
let size = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks = 8)
let marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner())
@ -55,7 +55,7 @@ twonodessuite "Marketplace", debug1 = false, debug2 = false:
let nodes = 5'u
# client 2 makes storage available
let startBalance = await token.balanceOf(account2)
let startBalanceHost = await token.balanceOf(account2)
discard client2.postAvailability(totalSize=size, duration=20*60.u256, minPrice=300.u256, maxCollateral=300.u256).get
# client 1 requests storage
@ -74,12 +74,18 @@ twonodessuite "Marketplace", debug1 = false, debug2 = false:
let purchase = client1.getPurchase(id).get
check purchase.error == none string
let clientBalanceBeforeFinished = await token.balanceOf(account1)
# Proving mechanism uses blockchain clock to do proving/collect/cleanup round
# hence we must use `advanceTime` over `sleepAsync` as Hardhat does mine new blocks
# only with new transaction
await ethProvider.advanceTime(duration)
check eventually (await token.balanceOf(account2)) - startBalance == duration*reward*nodes.u256
# Checking that the hosting node received reward for at least the time between <expiry;end>
check eventually (await token.balanceOf(account2)) - startBalanceHost >= (duration-5*60)*reward*nodes.u256
# Checking that client node receives some funds back that were not used for the host nodes
check eventually (await token.balanceOf(account1)) - clientBalanceBeforeFinished > 0
marketplacesuite "Marketplace payouts":

@ -1 +1 @@
Subproject commit f5a54c7ed4e9d562d984b02169a7e6ab2be973ba
Subproject commit 997696a20e0976011cdbc2f0ff3a844672056ba2