multinode test setup for invalid proof submission tests

# Conflicts:
#	codex/contracts/market.nim
#	codex/validation.nim
#	tests/integration/testproofs.nim
This commit is contained in:
Eric Mastro 2023-04-20 18:38:40 +10:00 committed by benbierens
parent 664a1b190c
commit 78fa4dd3a3
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
8 changed files with 358 additions and 151 deletions

View File

@ -4,6 +4,7 @@ import pkg/ethers
import pkg/ethers/testing
import pkg/upraises
import pkg/questionable
import pkg/chronicles
import ../market
import ./marketplace

View File

@ -7,6 +7,9 @@ import ../clock
export sets
logScope:
topics = "proving"
type
Proving* = ref object of RootObj
market*: Market
@ -31,7 +34,7 @@ proc `onProve=`*(proving: Proving, callback: OnProve) =
func add*(proving: Proving, slot: Slot) =
proving.slots.incl(slot)
proc getCurrentPeriod(proving: Proving): Future[Period] {.async.} =
proc getCurrentPeriod*(proving: Proving): Future[Period] {.async.} =
let periodicity = await proving.market.periodicity()
return periodicity.periodOf(proving.clock.now().u256)
@ -48,10 +51,14 @@ proc removeEndedContracts(proving: Proving) {.async.} =
proving.slots.excl(ended)
method prove*(proving: Proving, slot: Slot) {.base, async.} =
logScope:
currentPeriod = await proving.getCurrentPeriod()
without onProve =? proving.onProve:
raiseAssert "onProve callback not set"
try:
let proof = await onProve(slot)
debug "submitting proof"
await proving.market.submitProof(slot.id, proof)
except CatchableError as e:
error "Submitting proof failed", msg = e.msg

View File

@ -11,6 +11,9 @@ type
failEveryNProofs: uint
proofCount: uint
logScope:
topics = "simulated proving"
func new*(_: type SimulatedProving,
market: Market,
clock: Clock,
@ -27,21 +30,23 @@ method init(proving: SimulatedProving) {.async.} =
"--eth-provider."
proving.failEveryNProofs = 0'u
proc onSubmitProofError(error: ref CatchableError) =
error "Submitting invalid proof failed", msg = error.msg
proc onSubmitProofError(error: ref CatchableError, period: UInt256) =
error "Submitting invalid proof failed", period, msg = error.msg
method prove(proving: SimulatedProving, slot: Slot) {.async.} =
let period = await proving.getCurrentPeriod()
proving.proofCount += 1
if proving.failEveryNProofs > 0'u and
proving.proofCount mod proving.failEveryNProofs == 0'u:
proving.proofCount = 0
try:
debug "submitting INVALID proof"
await proving.market.submitProof(slot.id, newSeq[byte](0))
except ProviderError as e:
if not e.revertReason.contains("Invalid proof"):
onSubmitProofError(e)
onSubmitProofError(e, period)
except CatchableError as e:
onSubmitProofError(e)
onSubmitProofError(e, period)
else:
await procCall Proving(proving).prove(slot)

View File

@ -62,14 +62,18 @@ proc removeSlotsThatHaveEnded(validation: Validation) {.async.} =
proc markProofAsMissing(validation: Validation,
slotId: SlotId,
period: Period) {.async.} =
logScope:
currentPeriod = validation.getCurrentPeriod()
try:
if await validation.market.canProofBeMarkedAsMissing(slotId, period):
trace "Marking proof as missing", slotId = $slotId, period = period
trace "Marking proof as missing", slotId = $slotId, periodProofMissed = period
await validation.market.markProofAsMissing(slotId, period)
else: trace "Proof not missing", checkedPeriod = period
except CancelledError:
raise
except CatchableError as e:
debug "Marking proof as missing failed", msg = e.msg
error "Marking proof as missing failed", msg = e.msg
proc markProofsAsMissing(validation: Validation) {.async.} =
for slotId in validation.slots:

View File

@ -0,0 +1,122 @@
import std/os
import std/macros
import std/json
import std/httpclient
import pkg/chronicles
import ../ethertest
import ./codexclient
import ./nodes
export ethertest
export codexclient
export nodes
template multinodesuite*(name: string,
startNodes: StartNodes, debugNodes: DebugNodes, body: untyped) =
if (debugNodes.client or debugNodes.provider) and
(enabledLogLevel > LogLevel.DEBUG or
enabledLogLevel == LogLevel.NONE):
echo ""
echo "More test debug logging is available by running the tests with " &
"'-d:chronicles_log_level=DEBUG " &
"-d:chronicles_default_output_device=stdout " &
"-d:chronicles_sinks=textlines'"
echo ""
ethersuite name:
var running: seq[RunningNode]
var bootstrap: string
proc newNodeProcess(index: int,
addlOptions: seq[string],
debug: bool): (NodeProcess, string, Address) =
if index > accounts.len - 1:
raiseAssert("Cannot start node at index " & $index &
", not enough eth accounts.")
let datadir = getTempDir() / "Codex" & $index
var options = @[
"--api-port=" & $(8080 + index),
"--data-dir=" & datadir,
"--nat=127.0.0.1",
"--disc-ip=127.0.0.1",
"--disc-port=" & $(8090 + index),
"--eth-account=" & $accounts[index]]
.concat(addlOptions)
if debug: options.add "--log-level=INFO;DEBUG: " & debugNodes.topics
let node = startNode(options, debug = debug)
(node, datadir, accounts[index])
proc newCodexClient(index: int): CodexClient =
CodexClient.new("http://localhost:" & $(8080 + index) & "/api/codex/v1")
proc startClientNode() =
let index = running.len
let (node, datadir, account) = newNodeProcess(
index, @["--persistence"], debugNodes.client)
let restClient = newCodexClient(index)
running.add RunningNode.new(Role.Client, node, restClient, datadir,
account)
if debugNodes.client:
debug "started new client node and codex client",
restApiPort = 8080 + index, discPort = 8090 + index, account
proc startProviderNode(failEveryNProofs: uint = 0) =
let index = running.len
let (node, datadir, account) = newNodeProcess(index, @[
"--bootstrap-node=" & bootstrap,
"--persistence",
"--simulate-proof-failures=" & $failEveryNProofs],
debugNodes.provider)
let restClient = newCodexClient(index)
running.add RunningNode.new(Role.Provider, node, restClient, datadir,
account)
if debugNodes.provider:
debug "started new provider node and codex client",
restApiPort = 8080 + index, discPort = 8090 + index, account
proc startValidatorNode() =
let index = running.len
let (node, datadir, account) = newNodeProcess(index, @[
"--bootstrap-node=" & bootstrap,
"--validator"],
debugNodes.validator)
let restClient = newCodexClient(index)
running.add RunningNode.new(Role.Validator, node, restClient, datadir,
account)
if debugNodes.validator:
debug "started new validator node and codex client",
restApiPort = 8080 + index, discPort = 8090 + index, account
proc clients(): seq[RunningNode] =
running.filter(proc(r: RunningNode): bool = r.role == Role.Client)
proc providers(): seq[RunningNode] =
running.filter(proc(r: RunningNode): bool = r.role == Role.Provider)
proc validators(): seq[RunningNode] =
running.filter(proc(r: RunningNode): bool = r.role == Role.Validator)
setup:
for i in 0..<startNodes.clients:
startClientNode()
if i == 0:
bootstrap = running[0].restClient.info()["spr"].getStr()
for i in 0..<startNodes.providers:
startProviderNode()
for i in 0..<startNodes.validators:
startValidatorNode()
teardown:
for r in running:
r.restClient.close()
r.node.stop()
removeDir(r.datadir)
running = @[]
body

View File

@ -2,14 +2,58 @@ import std/osproc
import std/os
import std/streams
import std/strutils
import pkg/ethers
import ./codexclient
const workingDir = currentSourcePath() / ".." / ".." / ".."
const executable = "build" / "codex"
type NodeProcess* = ref object
type
NodeProcess* = ref object
process: Process
arguments: seq[string]
debug: bool
Role* = enum
Client,
Provider,
Validator
RunningNode* = ref object
role*: Role
node*: NodeProcess
restClient*: CodexClient
datadir*: string
ethAccount*: Address
StartNodes* = object
clients*: uint
providers*: uint
validators*: uint
DebugNodes* = object
client*: bool
provider*: bool
validator*: bool
topics*: string
proc new*(_: type RunningNode,
role: Role,
node: NodeProcess,
restClient: CodexClient,
datadir: string,
ethAccount: Address): RunningNode =
RunningNode(role: role,
node: node,
restClient: restClient,
datadir: datadir,
ethAccount: ethAccount)
proc init*(_: type StartNodes,
clients, providers, validators: uint): StartNodes =
StartNodes(clients: clients, providers: providers, validators: validators)
proc init*(_: type DebugNodes,
client, provider, validator: bool,
topics: string = "validation,proving"): DebugNodes =
DebugNodes(client: client, provider: provider, validator: validator,
topics: topics)
proc start(node: NodeProcess) =
if node.debug:

View File

@ -1,4 +1,7 @@
import std/sequtils
import std/os
from std/times import getTime, toUnix
import pkg/chronicles
import codex/contracts/marketplace
import codex/contracts/deployment
import codex/periods
@ -6,6 +9,10 @@ import ../contracts/time
import ../contracts/deployment
import ../codex/helpers/eventually
import ./twonodes
import ./multinodes
logScope:
topics = "test proofs"
twonodessuite "Proving integration test", debug1=false, debug2=false:
@ -96,7 +103,9 @@ twonodessuite "Proving integration test", debug1=false, debug2=false:
await subscription.unsubscribe()
stopValidator(validator)
invalidproofsuite "Simulate invalid proofs", debugClient=false, debugProvider=false:
multinodesuite "Simulate invalid proofs",
StartNodes.init(clients=1'u, providers=0'u, validators=1'u),
DebugNodes.init(client=false, provider=false, validator=false):
var marketplace: Marketplace
var period: uint64
@ -106,46 +115,41 @@ invalidproofsuite "Simulate invalid proofs", debugClient=false, debugProvider=fa
var missed: UInt256
var slotId: SlotId
var validator: NodeProcess
proc startValidator: NodeProcess =
startNode([
"--data-dir=" & validatorDir,
"--api-port=8180",
"--disc-port=8190",
"--validator",
"--eth-account=" & $accounts[2]
], debug = false)
proc stopValidator(node: NodeProcess) =
node.stop()
removeDir(validatorDir)
let validatorDir = getTempDir() / "CodexValidator"
var periodDowntime: uint8
var downtime: seq[(Period, bool)]
var periodicity: Periodicity
setup:
let deployment = Deployment.init()
marketplace = Marketplace.new(!deployment.address(Marketplace), provider)
period = (await marketplace.config()).proofs.period.truncate(uint64)
let config = await marketplace.config()
period = config.proofs.period.truncate(uint64)
periodDowntime = config.proofs.downtime
await provider.getSigner(accounts[0]).mint()
await provider.getSigner(accounts[1]).mint()
await provider.getSigner(accounts[1]).deposit()
await provider.getSigner(accounts[2]).mint()
await provider.getSigner(accounts[2]).deposit()
proofSubmitted = newFuture[void]("proofSubmitted")
proc onProofSubmitted(event: ProofSubmitted) =
debugEcho ">>> proof submitted: ", event.proof
submitted.add(event.proof)
proofSubmitted.complete()
proofSubmitted = newFuture[void]("proofSubmitted")
subscription = await marketplace.subscribe(ProofSubmitted, onProofSubmitted)
missed = 0.u256
slotId = SlotId(array[32, byte].default)
validator = startValidator()
slotId = SlotId(array[32, byte].default) # ensure we aren't reusing from prev test
downtime = @[]
periodicity = Periodicity(seconds: period.u256)
teardown:
await subscription.unsubscribe()
validator.stopValidator()
proc waitUntilPurchaseIsStarted(proofProbability: uint64 = 3,
duration: uint64 = 100 * period,
expiry: uint64 = 30) {.async.} =
proc getCurrentPeriod(): Future[Period] {.async.} =
return periodicity.periodOf(await provider.currentTime())
proc waitUntilPurchaseIsStarted(proofProbability: uint64 = 1,
duration: uint64 = 12 * period,
expiry: uint64 = 4 * period,
failEveryNProofs: uint): Future[Period] {.async.} =
if clients().len < 1 or providers().len < 1:
raiseAssert("must start at least one client and one provider")
@ -153,9 +157,15 @@ invalidproofsuite "Simulate invalid proofs", debugClient=false, debugProvider=fa
let client = clients()[0].restClient
let storageProvider = providers()[0].restClient
# The last period is the period in which the slot is freed, and therefore
# proofs cannot be submitted. That means that the duration must go an
# additional period longer to allow for invalid proofs to be submitted in
# the second to last period and counted as missed in the last period.
let dur = duration + (1 * period)
discard storageProvider.postAvailability(
size=0xFFFFF,
duration=duration,
duration=dur,
minPrice=300
)
let cid = client.upload("some file contents " & $ getTime().toUnix)
@ -163,56 +173,152 @@ invalidproofsuite "Simulate invalid proofs", debugClient=false, debugProvider=fa
let purchase = client.requestStorage(
cid,
expiry=expiry,
duration=duration,
duration=dur,
proofProbability=proofProbability,
reward=400
)
check eventually client.getPurchase(purchase){"state"} == %"started"
debug "purchase state", state = client.getPurchase(purchase){"state"}
let requestId = RequestId.fromHex client.getPurchase(purchase){"requestId"}.getStr
slotId = slotId(requestId, 0.u256)
return await getCurrentPeriod()
proc inDowntime: Future[bool] {.async.} =
var periodPointer = await marketplace.getPointer(slotId)
return periodPointer < periodDowntime
proc advanceToNextPeriod {.async.} =
let periodicity = Periodicity(seconds: period.u256)
let currentPeriod = periodicity.periodOf(await provider.currentTime())
let endOfPeriod = periodicity.periodEnd(currentPeriod)
await provider.advanceTimeTo(endOfPeriod + 1)
proc advanceToNextPeriodStart {.async.} =
let currentPeriod = await getCurrentPeriod()
let startOfPeriod = periodicity.periodEnd(currentPeriod + 1)
await provider.advanceTimeTo(startOfPeriod)
proc advanceToNextPeriod() {.async.} =
await provider.advanceTime(period.u256)
proc advanceToCurrentPeriodEnd {.async.} =
let currentPeriod = await getCurrentPeriod()
let endOfPeriod = periodicity.periodEnd(currentPeriod)
await provider.advanceTimeTo(endOfPeriod)
proc waitForProvingRounds(rounds: Positive) {.async.} =
var rnds = rounds - 1 # proof round runs prior to advancing
missed += await marketplace.missingProofs(slotId)
proc recordDowntime() {.async.} =
let currentPeriod = await getCurrentPeriod()
let isInDowntime = await inDowntime()
downtime.add (currentPeriod, isInDowntime)
debug "downtime recorded", currentPeriod, isInDowntime
while rnds > 0:
proc waitUntilSlotNoLongerFilled() {.async.} =
var i = 0
# await recordDowntime()
# await advanceToNextPeriodStart()
while (await marketplace.slotState(slotId)) == SlotState.Filled:
let currentPeriod = await getCurrentPeriod()
# await advanceToCurrentPeriodEnd()
await advanceToNextPeriod()
rnds -= 1
debug "--------------- PERIOD START ---------------", currentPeriod
await recordDowntime()
# debug "--------------- PERIOD END ---------------", currentPeriod
await sleepAsync(1.seconds) # let validation happen
debug "Checked previous period for missed proofs", missedProofs = $(await marketplace.missingProofs(slotId))
i += 1
# downtime.del downtime.len - 1 # remove last downtime as it is an additional proving round adding to duration for checking proofs, and we are offset by one as we are checking previous periods
missed = await marketplace.missingProofs(slotId)
debug "Total missed proofs", missed
proc invalid(proofs: seq[seq[byte]]): uint =
proofs.count(@[]).uint
proc expectedInvalid(startPeriod: Period,
failEveryNProofs: uint,
periods: Positive): seq[(Period, bool)] =
# Create a seq of bools where each bool represents a proving round.
# If true, an invalid proof should have been sent.
var p = startPeriod + 1.u256
var invalid: seq[(Period, bool)] = @[]
if failEveryNProofs == 0:
for i in 0..<periods:
p += 1.u256
invalid.add (p, false)
return invalid
test "simulates invalid proof every N proofs":
# TODO: waiting on validation work to be completed before these tests are possible
# 1. instantiate node manually (startNode) with --simulate-failed-proofs=3
# 2. check that the number of expected proofs are missed
let failEveryNProofs = 3
let totalProofs = 6
let expectedInvalid = totalProofs div failEveryNProofs
let expectedValid = totalProofs - expectedInvalid
startProviderNode(failEveryNProofs.uint)
for j in 0..<(periods div failEveryNProofs.int):
for i in 0..<failEveryNProofs - 1'u:
p += 1.u256
invalid.add (p, false)
p += 1.u256
invalid.add (p, true)
# add remaining falses
for k in 0..<(periods mod failEveryNProofs.int):
p += 1.u256
invalid.add (p, false)
# var proofs = false.repeat(failEveryNProofs - 1)
# proofs.add true
# proofs = proofs.cycle(periods div failEveryNProofs.int)
# .concat(false.repeat(periods mod failEveryNProofs.int)) # leftover falses
# return proofs
return invalid
await waitUntilPurchaseIsStarted(proofProbability=1)
await waitForProvingRounds(totalProofs)
proc expectedMissed(startPeriod: Period, failEveryNProofs: uint, periods: Positive): int =
# Intersects a seq of expected invalid proofs (true = invalid proof) with
# a seq of bools indicating a period was in pointer downtime (true = period
# was in pointer downtime).
# We can only expect an invalid proof to have been submitted if the slot
# was accepting proofs in that period, meaning it cannot be in downtime.
# eg failEveryNProofs = 3, periods = 2, the invalid proofs seq will be:
# @[false, false, true, false, false, true]
# If we hit downtime in the second half of running our test, the
# downtime seq might be @[false, false, false, true, true, true]
# When these two are intersected such that invalid is true and downtime is false,
# the result would be @[false, false, false, false, false, true], or 1 total
# invalid proof that should be marked as missed.
let invalid = expectedInvalid(startPeriod, failEveryNProofs, periods)
var expectedMissed = 0
for i in 0..<invalid.len:
let (invalidPeriod, isInvalidProof) = invalid[i]
for j in 0..<downtime.len:
let (downtimePeriod, isDowntime) = downtime[j]
if invalidPeriod == downtimePeriod:
if isInvalidProof and not isDowntime:
inc expectedMissed
break
# if invalid[i] == true and downtime[i] == false:
# expectedMissed += 1
check eventually submitted.len == expectedValid
check missed.truncate(int) == expectedInvalid
debug "expectedMissed invalid / downtime", invalid, downtime, expectedMissed
return expectedMissed
test "simulates invalid proof for every proofs":
let failEveryNProofs = 1'u
let totalProofs = 12
startProviderNode(failEveryNProofs)
# await waitUntilPurchaseIsStarted(proofProbability=1)
# var proofWasSubmitted = false
# proc onProofSubmitted(event: ProofSubmitted) =
# proofWasSubmitted = true
# let subscription = await marketplace.subscribe(ProofSubmitted, onProofSubmitted)
# await provider.advanceTime(period.u256)
# check eventually proofWasSubmitted
# await subscription.unsubscribe()
let startPeriod = await waitUntilPurchaseIsStarted(duration=totalProofs.uint * period,
failEveryNProofs = failEveryNProofs)
await waitUntilSlotNoLongerFilled()
# check 1 == 1
check missed.truncate(int) == expectedMissed(startPeriod, failEveryNProofs, totalProofs)
# test "simulates invalid proof every N proofs":
# let failEveryNProofs = 3'u
# let totalProofs = 12
# startProviderNode(failEveryNProofs)
# await waitUntilPurchaseIsStarted(duration=totalProofs.uint * period,
# failEveryNProofs = failEveryNProofs)
# await waitUntilSlotNoLongerFilled()
# check missed.truncate(int) == expectedMissed(failEveryNProofs, totalProofs)
# test "does not simulate invalid proofs when --simulate-failed-proofs is 0":
# let failEveryNProofs = 0'u
# let totalProofs = 12
# startProviderNode(failEveryNProofs)
# await waitUntilPurchaseIsStarted(duration=totalProofs.uint * period,
# failEveryNProofs = failEveryNProofs)
# await waitUntilSlotNoLongerFilled()
# check missed.truncate(int) == expectedMissed(failEveryNProofs, totalProofs)
# test "does not simulate invalid proof when --simulate-failed-proofs is 0":
# # 1. instantiate node manually (startNode) with --simulate-failed-proofs=0

View File

@ -77,85 +77,3 @@ template twonodessuite*(name: string, debug1, debug2: string, body) =
removeDir(dataDir2)
body
type
Role = enum
Client,
Provider
RunningNode* = ref object
role: Role
node: NodeProcess
restClient: CodexClient
datadir: string
template invalidproofsuite*(name: string, debugClient, debugProvider: bool, body) =
ethersuite name:
var running: seq[RunningNode]
var bootstrap: string
proc newNodeProcess(index: int,
addlOptions: seq[string],
debug: bool): (NodeProcess, string) =
let datadir = getTempDir() / "Codex" & $index
let node = startNode(@[
"--api-port=" & $(8080 + index),
"--data-dir=" & datadir,
"--nat=127.0.0.1",
"--disc-ip=127.0.0.1",
"--disc-port=" & $(8090 + index),
"--persistence",
"--eth-account=" & $accounts[index]
].concat(addlOptions), debug = debug)
debugEcho "started new codex node listening with rest api listening on port ", 8080 + index
(node, datadir)
proc newCodexClient(index: int): CodexClient =
debugEcho "started new rest client talking to port ", 8080 + index
CodexClient.new("http://localhost:" & $(8080 + index) & "/api/codex/v1")
proc startClientNode() =
let index = running.len
let (node, datadir) = newNodeProcess(index, @[], debugClient)
let restClient = newCodexClient(index)
running.add RunningNode(role: Role.Client,
node: node,
restClient: restClient,
datadir: datadir)
debugEcho "started client node, index ", index
proc startProviderNode(failEveryNProofs: uint) =
let index = running.len
let (node, datadir) = newNodeProcess(index, @[
"--bootstrap-node=" & bootstrap,
"--simulate-proof-failures=" & $failEveryNProofs],
debugProvider)
let restClient = newCodexClient(index)
running.add RunningNode(role: Role.Provider,
node: node,
restClient: restClient,
datadir: datadir)
debugEcho "started provider node, index ", index
proc clients(): seq[RunningNode] =
running.filter(proc(r: RunningNode): bool = r.role == Role.Client)
proc providers(): seq[RunningNode] =
running.filter(proc(r: RunningNode): bool = r.role == Role.Provider)
setup:
startClientNode()
bootstrap = running[0].restClient.info()["spr"].getStr()
teardown:
for r in running:
r.restClient.close()
r.node.stop()
removeDir(r.datadir)
body