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