From 03fa3706244695f4bfa22afd024434495c7ef0c6 Mon Sep 17 00:00:00 2001 From: markspanbroek Date: Fri, 8 Apr 2022 23:58:16 +0200 Subject: [PATCH] Proving (#66) * Add Proving object, which maintains contract id's to watch * [proving] invoke callback when proof is required # Conflicts: # dagger/por/timing/periods.nim # dagger/por/timing/prooftiming.nim * [proving] check proof requirements for all our contracts # Conflicts: # tests/dagger/helpers/mockprooftiming.nim * Update vendor/dagger-contracts * [proving] call onProofRequired() when proof is required soon * [proving] stop checking contracts that have ended * [proving] Remove duplicated funcs * [proving] Implement ProofTiming on top of smart contract * [proving] Fix race condition in waitUntilNextPeriod() Sometimes waitUntilNextPeriod would take a while to determine the current period, leading to unexpected results. Splits waitUntilNextPeriod() into getCurrentPeriod() and waitUntilPeriod(), to ensure that we're really waiting for the period that we think we're waiting for. --- dagger/contracts.nim | 2 + dagger/contracts/prooftiming.nim | 40 ++++++++ dagger/contracts/storage.nim | 1 + .../por/timing}/periods.nim | 0 dagger/por/timing/prooftiming.nim | 35 +++++++ dagger/proving.nim | 50 ++++++++++ tests/contracts/testContracts.nim | 2 +- tests/contracts/testProofTiming.nim | 47 ++++++++++ tests/dagger/helpers/mockprooftiming.nim | 72 +++++++++++++++ tests/dagger/testproving.nim | 91 +++++++++++++++++++ tests/testContracts.nim | 1 + tests/testDagger.nim | 1 + vendor/dagger-contracts | 2 +- 13 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 dagger/contracts/prooftiming.nim rename {tests/contracts => dagger/por/timing}/periods.nim (100%) create mode 100644 dagger/por/timing/prooftiming.nim create mode 100644 dagger/proving.nim create mode 100644 tests/contracts/testProofTiming.nim create mode 100644 tests/dagger/helpers/mockprooftiming.nim create mode 100644 tests/dagger/testproving.nim diff --git a/dagger/contracts.nim b/dagger/contracts.nim index b0ba4a07..8aed6b0f 100644 --- a/dagger/contracts.nim +++ b/dagger/contracts.nim @@ -3,9 +3,11 @@ import contracts/offers import contracts/storage import contracts/deployment import contracts/market +import contracts/prooftiming export requests export offers export storage export deployment export market +export prooftiming diff --git a/dagger/contracts/prooftiming.nim b/dagger/contracts/prooftiming.nim new file mode 100644 index 00000000..3688c634 --- /dev/null +++ b/dagger/contracts/prooftiming.nim @@ -0,0 +1,40 @@ +import ../por/timing/prooftiming +import ./storage + +export prooftiming + +type + OnChainProofTiming* = ref object of ProofTiming + storage: Storage + pollInterval*: Duration + +const DefaultPollInterval = 3.seconds + +proc new*(_: type OnChainProofTiming, storage: Storage): OnChainProofTiming = + OnChainProofTiming(storage: storage, pollInterval: DefaultPollInterval) + +method periodicity*(timing: OnChainProofTiming): Future[Periodicity] {.async.} = + let period = await timing.storage.proofPeriod() + return Periodicity(seconds: period) + +method getCurrentPeriod*(timing: OnChainProofTiming): Future[Period] {.async.} = + let periodicity = await timing.periodicity() + let blk = !await timing.storage.provider.getBlock(BlockTag.latest) + return periodicity.periodOf(blk.timestamp) + +method waitUntilPeriod*(timing: OnChainProofTiming, + period: Period) {.async.} = + while (await timing.getCurrentPeriod()) < period: + await sleepAsync(timing.pollInterval) + +method isProofRequired*(timing: OnChainProofTiming, + id: ContractId): Future[bool] {.async.} = + return await timing.storage.isProofRequired(id) + +method willProofBeRequired*(timing: OnChainProofTiming, + id: ContractId): Future[bool] {.async.} = + return await timing.storage.willProofBeRequired(id) + +method getProofEnd*(timing: OnChainProofTiming, + id: ContractId): Future[UInt256] {.async.} = + return await timing.storage.proofEnd(id) diff --git a/dagger/contracts/storage.nim b/dagger/contracts/storage.nim index e101faea..89b47f0d 100644 --- a/dagger/contracts/storage.nim +++ b/dagger/contracts/storage.nim @@ -43,6 +43,7 @@ proc proofTimeout*(storage: Storage): UInt256 {.contract, view.} proc proofEnd*(storage: Storage, id: Id): UInt256 {.contract, view.} proc missingProofs*(storage: Storage, id: Id): UInt256 {.contract, view.} proc isProofRequired*(storage: Storage, id: Id): bool {.contract, view.} +proc willProofBeRequired*(storage: Storage, id: Id): bool {.contract, view.} proc getChallenge*(storage: Storage, id: Id): array[32, byte] {.contract, view.} proc getPointer*(storage: Storage, id: Id): uint8 {.contract, view.} diff --git a/tests/contracts/periods.nim b/dagger/por/timing/periods.nim similarity index 100% rename from tests/contracts/periods.nim rename to dagger/por/timing/periods.nim diff --git a/dagger/por/timing/prooftiming.nim b/dagger/por/timing/prooftiming.nim new file mode 100644 index 00000000..54d3e03a --- /dev/null +++ b/dagger/por/timing/prooftiming.nim @@ -0,0 +1,35 @@ +import pkg/chronos +import pkg/stint +import ./periods + +export chronos +export stint +export periods + +type + ProofTiming* = ref object of RootObj + ContractId* = array[32, byte] + +method periodicity*(proofTiming: ProofTiming): + Future[Periodicity] {.base, async.} = + raiseAssert("not implemented") + +method getCurrentPeriod*(proofTiming: ProofTiming): + Future[Period] {.base, async.} = + raiseAssert("not implemented") + +method waitUntilPeriod*(proofTiming: ProofTiming, + period: Period) {.base, async.} = + raiseAssert("not implemented") + +method isProofRequired*(proofTiming: ProofTiming, + id: ContractId): Future[bool] {.base, async.} = + raiseAssert("not implemented") + +method willProofBeRequired*(proofTiming: ProofTiming, + id: ContractId): Future[bool] {.base, async.} = + raiseAssert("not implemented") + +method getProofEnd*(proofTiming: ProofTiming, + id: ContractId): Future[UInt256] {.base, async.} = + raiseAssert("not implemented") diff --git a/dagger/proving.nim b/dagger/proving.nim new file mode 100644 index 00000000..945de5c0 --- /dev/null +++ b/dagger/proving.nim @@ -0,0 +1,50 @@ +import std/sets +import std/times +import pkg/upraises +import pkg/questionable +import ./por/timing/prooftiming + +export sets +export prooftiming + +type + Proving* = ref object + timing: ProofTiming + stopped: bool + contracts*: HashSet[ContractId] + onProofRequired: ?OnProofRequired + OnProofRequired* = proc (id: ContractId) {.gcsafe, upraises:[].} + +func new*(_: type Proving, timing: ProofTiming): Proving = + Proving(timing: timing) + +proc `onProofRequired=`*(proving: Proving, callback: OnProofRequired) = + proving.onProofRequired = some callback + +func add*(proving: Proving, id: ContractId) = + proving.contracts.incl(id) + +proc removeEndedContracts(proving: Proving) {.async.} = + let now = getTime().toUnix().u256 + var ended: HashSet[ContractId] + for id in proving.contracts: + if now >= (await proving.timing.getProofEnd(id)): + ended.incl(id) + proving.contracts.excl(ended) + +proc run(proving: Proving) {.async.} = + while not proving.stopped: + let currentPeriod = await proving.timing.getCurrentPeriod() + await proving.removeEndedContracts() + for id in proving.contracts: + if (await proving.timing.isProofRequired(id)) or + (await proving.timing.willProofBeRequired(id)): + if callback =? proving.onProofRequired: + callback(id) + await proving.timing.waitUntilPeriod(currentPeriod + 1) + +proc start*(proving: Proving) = + asyncSpawn proving.run() + +proc stop*(proving: Proving) = + proving.stopped = true diff --git a/tests/contracts/testContracts.nim b/tests/contracts/testContracts.nim index bd2e8562..2513586d 100644 --- a/tests/contracts/testContracts.nim +++ b/tests/contracts/testContracts.nim @@ -3,10 +3,10 @@ import pkg/chronos import pkg/nimcrypto import dagger/contracts import dagger/contracts/testtoken +import dagger/por/timing/periods import ./ethertest import ./examples import ./time -import ./periods ethersuite "Storage contracts": diff --git a/tests/contracts/testProofTiming.nim b/tests/contracts/testProofTiming.nim new file mode 100644 index 00000000..2e6fa3c4 --- /dev/null +++ b/tests/contracts/testProofTiming.nim @@ -0,0 +1,47 @@ +import ./ethertest +import dagger/contracts +import ./examples +import ./time + +ethersuite "On-Chain Proof Timing": + + var timing: OnChainProofTiming + var storage: Storage + + setup: + let deployment = deployment() + storage = Storage.new(!deployment.address(Storage), provider) + timing = OnChainProofTiming.new(storage) + + test "can retrieve proof periodicity": + let periodicity = await timing.periodicity() + let periodLength = await storage.proofPeriod() + check periodicity.seconds == periodLength + + test "supports waiting until next period": + let periodicity = await timing.periodicity() + let currentPeriod = await timing.getCurrentPeriod() + + let pollInterval = 200.milliseconds + timing.pollInterval = pollInterval + + proc waitForPoll {.async.} = + await sleepAsync(pollInterval * 2) + + let future = timing.waitUntilPeriod(currentPeriod + 1) + + check not future.completed + + await provider.advanceTimeTo(periodicity.periodEnd(currentPeriod)) + await waitForPoll() + + check future.completed + + test "supports checking whether proof is required now": + check (await timing.isProofRequired(ContractId.example)) == false + + test "supports checking whether proof is required soon": + check (await timing.willProofBeRequired(ContractId.example)) == false + + test "retrieves proof end time": + check (await timing.getProofEnd(ContractId.example)) == 0.u256 diff --git a/tests/dagger/helpers/mockprooftiming.nim b/tests/dagger/helpers/mockprooftiming.nim new file mode 100644 index 00000000..3ab7fe6c --- /dev/null +++ b/tests/dagger/helpers/mockprooftiming.nim @@ -0,0 +1,72 @@ +import std/sets +import std/tables +import pkg/dagger/por/timing/prooftiming + +type + MockProofTiming* = ref object of ProofTiming + periodicity: Periodicity + currentPeriod: Period + waiting: Table[Period, seq[Future[void]]] + proofsRequired: HashSet[ContractId] + proofsToBeRequired: HashSet[ContractId] + proofEnds: Table[ContractId, UInt256] + +const DefaultPeriodLength = 10.u256 + +func new*(_: type MockProofTiming): MockProofTiming = + MockProofTiming(periodicity: Periodicity(seconds: DefaultPeriodLength)) + +func setPeriodicity*(mock: MockProofTiming, periodicity: Periodicity) = + mock.periodicity = periodicity + +method periodicity*(mock: MockProofTiming): Future[Periodicity] {.async.} = + return mock.periodicity + +proc setProofRequired*(mock: MockProofTiming, id: ContractId, required: bool) = + if required: + mock.proofsRequired.incl(id) + else: + mock.proofsRequired.excl(id) + +method isProofRequired*(mock: MockProofTiming, + id: ContractId): Future[bool] {.async.} = + return mock.proofsRequired.contains(id) + +proc setProofToBeRequired*(mock: MockProofTiming, id: ContractId, required: bool) = + if required: + mock.proofsToBeRequired.incl(id) + else: + mock.proofsToBeRequired.excl(id) + +method willProofBeRequired*(mock: MockProofTiming, + id: ContractId): Future[bool] {.async.} = + return mock.proofsToBeRequired.contains(id) + +proc setProofEnd*(mock: MockProofTiming, id: ContractId, proofEnd: UInt256) = + mock.proofEnds[id] = proofEnd + +method getProofEnd*(mock: MockProofTiming, + id: ContractId): Future[UInt256] {.async.} = + if mock.proofEnds.hasKey(id): + return mock.proofEnds[id] + else: + return UInt256.high + +proc advanceToPeriod*(mock: MockProofTiming, period: Period) = + doAssert period >= mock.currentPeriod + for key in mock.waiting.keys: + if key <= period: + for future in mock.waiting[key]: + future.complete() + mock.waiting[key] = @[] + +method getCurrentPeriod*(mock: MockProofTiming): Future[Period] {.async.} = + return mock.currentPeriod + +method waitUntilPeriod*(mock: MockProofTiming, period: Period) {.async.} = + if period > mock.currentPeriod: + let future = Future[void]() + if not mock.waiting.hasKey(period): + mock.waiting[period] = @[] + mock.waiting[period].add(future) + await future diff --git a/tests/dagger/testproving.nim b/tests/dagger/testproving.nim new file mode 100644 index 00000000..79c4fb35 --- /dev/null +++ b/tests/dagger/testproving.nim @@ -0,0 +1,91 @@ +from std/times import getTime, toUnix +import pkg/asynctest +import pkg/chronos +import pkg/dagger/proving +import ./helpers/mockprooftiming +import ./examples + +suite "Proving": + + var proving: Proving + var timing: MockProofTiming + + setup: + timing = MockProofTiming.new() + proving = Proving.new(timing) + proving.start() + + teardown: + proving.stop() + + proc advanceToNextPeriod(timing: MockProofTiming) {.async.} = + let current = await timing.getCurrentPeriod() + timing.advanceToPeriod(current + 1) + await sleepAsync(1.milliseconds) + + test "maintains a list of contract ids to watch": + let id1, id2 = ContractId.example + check proving.contracts.len == 0 + proving.add(id1) + check proving.contracts.contains(id1) + proving.add(id2) + check proving.contracts.contains(id1) + check proving.contracts.contains(id2) + + test "removes duplicate contract ids": + let id = ContractId.example + proving.add(id) + proving.add(id) + check proving.contracts.len == 1 + + test "invokes callback when proof is required": + let id = ContractId.example + proving.add(id) + var called: bool + proc onProofRequired(id: ContractId) = + called = true + proving.onProofRequired = onProofRequired + timing.setProofRequired(id, true) + await timing.advanceToNextPeriod() + check called + + test "callback receives id of contract for which proof is required": + let id1, id2 = ContractId.example + proving.add(id1) + proving.add(id2) + var callbackIds: seq[ContractId] + proc onProofRequired(id: ContractId) = + callbackIds.add(id) + proving.onProofRequired = onProofRequired + timing.setProofRequired(id1, true) + await timing.advanceToNextPeriod() + check callbackIds == @[id1] + timing.setProofRequired(id1, false) + timing.setProofRequired(id2, true) + await timing.advanceToNextPeriod() + check callbackIds == @[id1, id2] + + test "invokes callback when proof is about to be required": + let id = ContractId.example + proving.add(id) + var called: bool + proc onProofRequired(id: ContractId) = + called = true + proving.onProofRequired = onProofRequired + timing.setProofRequired(id, false) + timing.setProofToBeRequired(id, true) + await timing.advanceToNextPeriod() + check called + + test "stops watching when contract has ended": + let id = ContractId.example + proving.add(id) + timing.setProofEnd(id, getTime().toUnix().u256) + await timing.advanceToNextPeriod() + var called: bool + proc onProofRequired(id: ContractId) = + called = true + proving.onProofRequired = onProofRequired + timing.setProofRequired(id, true) + await timing.advanceToNextPeriod() + check not called diff --git a/tests/testContracts.nim b/tests/testContracts.nim index 8dad4da5..30a4bd5b 100644 --- a/tests/testContracts.nim +++ b/tests/testContracts.nim @@ -1,5 +1,6 @@ import ./contracts/testCollateral import ./contracts/testContracts import ./contracts/testMarket +import ./contracts/testProofTiming {.warning[UnusedImport]:off.} diff --git a/tests/testDagger.nim b/tests/testDagger.nim index 99dced9a..332baa0a 100644 --- a/tests/testDagger.nim +++ b/tests/testDagger.nim @@ -8,6 +8,7 @@ import ./dagger/teststorestream import ./dagger/testpurchasing import ./dagger/testsales import ./dagger/testerasure +import ./dagger/testproving # to check that everything compiles import ../dagger diff --git a/vendor/dagger-contracts b/vendor/dagger-contracts index 0587c2d5..29b57759 160000 --- a/vendor/dagger-contracts +++ b/vendor/dagger-contracts @@ -1 +1 @@ -Subproject commit 0587c2d585bef88575b10c63a4eddb756b5dde2e +Subproject commit 29b5775951e774cb170b23cb6772cd7f1e7b5499