nimbus-eth2/tests/test_validator_client.nim

527 lines
20 KiB
Nim

# beacon_chain
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [].}
{.used.}
import std/strutils
import unittest2
import ../beacon_chain/validator_client/common
const
HostNames = [
"[2001:db8::1]",
"127.0.0.1",
"hostname.com",
"localhost",
"username:password@[2001:db8::1]",
"username:password@127.0.0.1",
"username:password@hostname.com",
"username:password@localhost",
]
GoodTestVectors = [
("http://$1",
"ok(http://$1)"),
("http://$1?q=query",
"ok(http://$1?q=query)"),
("http://$1?q=query#anchor",
"ok(http://$1?q=query#anchor)"),
("http://$1/subpath/",
"ok(http://$1/subpath/)"),
("http://$1/subpath/q=query",
"ok(http://$1/subpath/q=query)"),
("http://$1/subpath/q=query#anchor",
"ok(http://$1/subpath/q=query#anchor)"),
("http://$1/subpath",
"ok(http://$1/subpath)"),
("http://$1/subpath?q=query",
"ok(http://$1/subpath?q=query)"),
("http://$1/subpath?q=query#anchor",
"ok(http://$1/subpath?q=query#anchor)"),
("https://$1",
"ok(https://$1)"),
("https://$1?q=query",
"ok(https://$1?q=query)"),
("https://$1?q=query#anchor",
"ok(https://$1?q=query#anchor)"),
("https://$1/subpath/",
"ok(https://$1/subpath/)"),
("https://$1/subpath/q=query",
"ok(https://$1/subpath/q=query)"),
("https://$1/subpath/q=query#anchor",
"ok(https://$1/subpath/q=query#anchor)"),
("https://$1/subpath",
"ok(https://$1/subpath)"),
("https://$1/subpath?q=query",
"ok(https://$1/subpath?q=query)"),
("https://$1/subpath?q=query#anchor",
"ok(https://$1/subpath?q=query#anchor)"),
("$1:5052",
"ok(http://$1:5052)"),
("$1:5052?q=query",
"ok(http://$1:5052?q=query)"),
("$1:5052?q=query#anchor",
"ok(http://$1:5052?q=query#anchor)"),
("$1:5052/subpath/",
"ok(http://$1:5052/subpath/)"),
("$1:5052/subpath/q=query",
"ok(http://$1:5052/subpath/q=query)"),
("$1:5052/subpath/q=query#anchor",
"ok(http://$1:5052/subpath/q=query#anchor)"),
("$1:5052/subpath",
"ok(http://$1:5052/subpath)"),
("$1:5052/subpath?q=query",
"ok(http://$1:5052/subpath?q=query)"),
("$1:5052/subpath?q=query#anchor",
"ok(http://$1:5052/subpath?q=query#anchor)"),
("bnode://$1:5052",
"err(Unknown scheme value)"),
("bnode://$1:5052?q=query",
"err(Unknown scheme value)"),
("bnode://$1:5052?q=query#anchor",
"err(Unknown scheme value)"),
("bnode://$1:5052/subpath/",
"err(Unknown scheme value)"),
("bnode://$1:5052/subpath/q=query",
"err(Unknown scheme value)"),
("bnode://$1:5052/subpath/q=query#anchor",
"err(Unknown scheme value)"),
("bnode://$1:5052/subpath",
"err(Unknown scheme value)"),
("bnode://$1:5052/subpath?q=query",
"err(Unknown scheme value)"),
("bnode://$1:5052/subpath?q=query#anchor",
"err(Unknown scheme value)"),
("//$1:5052",
"ok(http://$1:5052)"),
("//$1:5052?q=query",
"ok(http://$1:5052?q=query)"),
("//$1:5052?q=query#anchor",
"ok(http://$1:5052?q=query#anchor)"),
("//$1:5052/subpath/",
"ok(http://$1:5052/subpath/)"),
("//$1:5052/subpath/q=query",
"ok(http://$1:5052/subpath/q=query)"),
("//$1:5052/subpath/q=query#anchor",
"ok(http://$1:5052/subpath/q=query#anchor)"),
("//$1:5052/subpath",
"ok(http://$1:5052/subpath)"),
("//$1:5052/subpath?q=query",
"ok(http://$1:5052/subpath?q=query)"),
("//$1:5052/subpath?q=query#anchor",
"ok(http://$1:5052/subpath?q=query#anchor)"),
("//$1", "err(Missing port number)"),
("//$1?q=query", "err(Missing port number)"),
("//$1?q=query#anchor", "err(Missing port number)"),
("//$1/subpath/", "err(Missing port number)"),
("//$1/subpath/q=query", "err(Missing port number)"),
("//$1/subpath/q=query#anchor", "err(Missing port number)"),
("//$1/subpath", "err(Missing port number)"),
("//$1/subpath?q=query", "err(Missing port number)"),
("//$1/subpath?q=query#anchor", "err(Missing port number)"),
("$1", "err(Missing port number)"),
("$1?q=query", "err(Missing port number)"),
("$1?q=query#anchor", "err(Missing port number)"),
("$1/subpath/", "err(Missing port number)"),
("$1/subpath/q=query", "err(Missing port number)"),
("$1/subpath/q=query#anchor", "err(Missing port number)"),
("$1/subpath", "err(Missing port number)"),
("$1/subpath?q=query", "err(Missing port number)"),
("$1/subpath?q=query#anchor", "err(Missing port number)"),
("", "err(Missing hostname)")
]
ObolBeaconRequestTestVector = """
[
{
"validator_index": "1",
"slot": "1",
"selection_proof": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"slot": "2",
"validator_index": "2",
"selection_proof": "0x2b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "3",
"selection_proof": "0x3b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
"slot": "3"
},
{
"selection_proof": "0x4b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
"validator_index": "4",
"slot": "4"
}
]"""
ObolBeaconResponseTestVector = """
{
"data": [
{
"validator_index": "1",
"slot": "1",
"selection_proof": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "2",
"slot": "2",
"selection_proof": "0x2b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "3",
"slot": "3",
"selection_proof": "0x3b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "4",
"slot": "4",
"selection_proof": "0x4b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
}
]
}"""
ObolBeaconResponseTestVectorObject = [
(
validator_index: RestValidatorIndex(1),
slot: Slot(1),
selection_proof: "1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
),
(
validator_index: RestValidatorIndex(2),
slot: Slot(2),
selection_proof: "2b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
),
(
validator_index: RestValidatorIndex(3),
slot: Slot(3),
selection_proof: "3b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
),
(
validator_index: RestValidatorIndex(4),
slot: Slot(4),
selection_proof: "4b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
)
]
ObolSyncRequestTestVector = """
[
{
"validator_index": "1",
"slot": "1",
"subcommittee_index": "1",
"selection_proof": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "2",
"subcommittee_index": "2",
"slot": "2",
"selection_proof": "0x2b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"subcommittee_index": "3",
"validator_index": "3",
"slot": "3",
"selection_proof": "0x3b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "4",
"slot": "4",
"selection_proof": "0x4b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
"subcommittee_index": "4"
}
]"""
ObolSyncResponseTestVector = """
{
"data": [
{
"validator_index": "1",
"slot": "1",
"subcommittee_index": "1",
"selection_proof": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "2",
"subcommittee_index": "2",
"slot": "2",
"selection_proof": "0x2b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"subcommittee_index": "3",
"validator_index": "3",
"slot": "3",
"selection_proof": "0x3b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
},
{
"validator_index": "4",
"slot": "4",
"selection_proof": "0x4b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
"subcommittee_index": "4"
}
]
}"""
ObolSyncResponseTestVectorObject = [
(
validator_index: RestValidatorIndex(1),
slot: Slot(1),
subcommittee_index: 1'u64,
selection_proof: "1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
),
(
validator_index: RestValidatorIndex(2),
slot: Slot(2),
subcommittee_index: 2'u64,
selection_proof: "2b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
),
(
validator_index: RestValidatorIndex(3),
slot: Slot(3),
subcommittee_index: 3'u64,
selection_proof: "3b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
),
(
validator_index: RestValidatorIndex(4),
slot: Slot(4),
subcommittee_index: 4'u64,
selection_proof: "4b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
)
]
type
TestDecodeTypes = seq[RestBeaconCommitteeSelection] |
seq[RestSyncCommitteeSelection]
suite "Validator Client test suite":
proc decodeBytes[T: TestDecodeTypes](
t: typedesc[T],
value: openArray[byte],
contentType: Opt[ContentTypeData] = Opt.none(ContentTypeData)
): RestResult[T] =
let mediaType =
if contentType.isNone():
ApplicationJsonMediaType
else:
if isWildCard(contentType.get().mediaType):
return err("Incorrect Content-Type")
contentType.get().mediaType
if mediaType == ApplicationJsonMediaType:
try:
ok RestJson.decode(value, T,
requireAllFields = true,
allowUnknownFields = true)
except SerializationError as exc:
err("Serialization error")
else:
err("Content-Type not supported")
proc submitBeaconCommitteeSelectionsPlain*(
body: seq[RestBeaconCommitteeSelection]
): RestPlainResponse {.
rest, endpoint: "/eth/v1/validator/beacon_committee_selections",
meth: MethodPost.}
## https://ethereum.github.io/beacon-APIs/#/Validator/submitBeaconCommitteeSelections
proc submitSyncCommitteeSelectionsPlain*(
body: seq[RestSyncCommitteeSelection]
): RestPlainResponse {.
rest, endpoint: "/eth/v1/validator/sync_committee_selections",
meth: MethodPost.}
## https://ethereum.github.io/beacon-APIs/#/Validator/submitSyncCommitteeSelections
proc createServer(address: TransportAddress,
process: HttpProcessCallback, secure: bool): HttpServerRef =
let
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
res = HttpServerRef.new(address, process, socketFlags = socketFlags)
res.get()
test "normalizeUri() test vectors":
for hostname in HostNames:
for vector in GoodTestVectors:
let expect = vector[1] % (hostname)
check $normalizeUri(parseUri(vector[0] % (hostname))) == expect
asyncTest "/eth/v1/validator/sync_committee_selections " &
"serialization/deserialization test":
var clientRequest: seq[byte]
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
if r.isOk():
let request = r.get()
case request.uri.path
of "/eth/v1/validator/beacon_committee_selections":
clientRequest = await request.getBody()
let headers = HttpTable.init([("Content-Type", "application/json")])
return await request.respond(Http200, ObolBeaconResponseTestVector,
headers)
else:
return await request.respond(Http404, "Page not found")
else:
return dumbResponse()
let server = createServer(initTAddress("127.0.0.1:0"), process, false)
server.start()
defer:
await server.stop()
await server.closeWait()
let
serverAddress = server.instance.localAddress
flags = {RestClientFlag.CommaSeparatedArray}
remoteUri = "http://" & $serverAddress
client =
block:
let res = RestClientRef.new(remoteUri, flags = flags)
check res.isOk()
res.get()
selections =
block:
let res = decodeBytes(
seq[RestBeaconCommitteeSelection],
ObolBeaconRequestTestVector.toOpenArrayByte(
0, len(ObolBeaconRequestTestVector) - 1))
check res.isOk()
res.get()
defer:
await client.closeWait()
let resp = await client.submitBeaconCommitteeSelectionsPlain(selections)
check:
resp.status == 200
resp.contentType == MediaType.init("application/json")
let request =
block:
let res = decodeBytes(
seq[RestBeaconCommitteeSelection],
clientRequest)
check res.isOk()
res.get()
let response = block:
let res = decodeBytes(SubmitBeaconCommitteeSelectionsResponse,
resp.data, resp.contentType)
check res.isOk()
res.get()
check:
len(request) == len(selections)
len(response.data) == len(ObolBeaconResponseTestVectorObject)
# Checking response
for index, item in response.data.pairs():
check:
item.validator_index ==
ObolBeaconResponseTestVectorObject[index].validator_index
item.slot ==
ObolBeaconResponseTestVectorObject[index].slot
item.selection_proof.toHex() ==
ObolBeaconResponseTestVectorObject[index].selection_proof
# Checking request
for index, item in selections.pairs():
check:
item.validator_index == request[index].validator_index
item.slot == request[index].slot
item.selection_proof.toHex() == request[index].selection_proof.toHex()
asyncTest "/eth/v1/validator/sync_committee_selections " &
"serialization/deserialization test":
var clientRequest: seq[byte]
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
if r.isOk():
let request = r.get()
case request.uri.path
of "/eth/v1/validator/sync_committee_selections":
clientRequest = await request.getBody()
let headers = HttpTable.init([("Content-Type", "application/json")])
return await request.respond(Http200, ObolSyncResponseTestVector,
headers)
else:
return await request.respond(Http404, "Page not found")
else:
return dumbResponse()
let server = createServer(initTAddress("127.0.0.1:0"), process, false)
server.start()
defer:
await server.stop()
await server.closeWait()
let
serverAddress = server.instance.localAddress
flags = {RestClientFlag.CommaSeparatedArray}
remoteUri = "http://" & $serverAddress
client =
block:
let res = RestClientRef.new(remoteUri, flags = flags)
check res.isOk()
res.get()
selections =
block:
let res = decodeBytes(
seq[RestSyncCommitteeSelection],
ObolSyncRequestTestVector.toOpenArrayByte(
0, len(ObolSyncRequestTestVector) - 1))
check res.isOk()
res.get()
defer:
await client.closeWait()
let resp = await client.submitSyncCommitteeSelectionsPlain(selections)
check:
resp.status == 200
resp.contentType == MediaType.init("application/json")
let request =
block:
let res = decodeBytes(
seq[RestSyncCommitteeSelection],
clientRequest)
check res.isOk()
res.get()
let response = block:
let res = decodeBytes(SubmitSyncCommitteeSelectionsResponse,
resp.data, resp.contentType)
check res.isOk()
res.get()
check:
len(request) == len(selections)
len(response.data) == len(ObolSyncResponseTestVectorObject)
# Checking response
for index, item in response.data.pairs():
check:
item.validator_index ==
ObolSyncResponseTestVectorObject[index].validator_index
item.slot ==
ObolSyncResponseTestVectorObject[index].slot
item.selection_proof.toHex() ==
ObolSyncResponseTestVectorObject[index].selection_proof
item.subcommittee_index == request[index].subcommittee_index
# Checking request
for index, item in selections.pairs():
check:
item.validator_index == request[index].validator_index
item.slot == request[index].slot
item.subcommittee_index == request[index].subcommittee_index
item.selection_proof.toHex() == request[index].selection_proof.toHex()