# 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 httputils import chronos/unittest2/asynctests import ../beacon_chain/validator_client/[api, common, scoring, fallback_service] 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)") ] type AttestationDataTuple* = tuple[ slot: uint64, index: uint64, beacon_block_root: string, source: uint64, target: uint64 ] const AttestationDataVectors = [ # Attestation score with block monitoring enabled (perfect). ((6002798'u64, 10'u64, "22242212", 187586'u64, 187587'u64), ("22242212", 6002798'u64), ""), ((6002811'u64, 24'u64, "26ec78d6", 187586'u64, 187587'u64), ("26ec78d6", 6002811'u64), ""), ((6002821'u64, 11'u64, "10c6d1a2", 187587'u64, 187588'u64), ("10c6d1a2", 6002821'u64), ""), ((6002836'u64, 15'u64, "42354ded", 187587'u64, 187588'u64), ("42354ded", 6002836'u64), ""), ((6002859'u64, 10'u64, "97d8ac69", 187588'u64, 187589'u64), ("97d8ac69", 6002859'u64), ""), # Attestation score with block monitoring enabled #1 (not perfect). ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), ("524a9e2b", 6002870'u64), "375177.5000"), ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), ("524a9e2b", 6002869'u64), "375177.3333"), ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), ("524a9e2b", 6002868'u64), "375177.2500"), ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), ("524a9e2b", 6002867'u64), "375177.2000"), ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), ("524a9e2b", 6002866'u64), "375177.1667"), # Attestation score with block monitoring enabled #2 (not perfect). ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), ("22a19d87", 6002961'u64), "375183.5000"), ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), ("22a19d87", 6002960'u64), "375183.3333"), ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), ("22a19d87", 6002959'u64), "375183.2500"), ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), ("22a19d87", 6002958'u64), "375183.2000"), ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), ("22a19d87", 6002957'u64), "375183.1667"), # Attestation score with block monitoring disabled #1. ((6003217'u64, 52'u64, "5e945218", 187599'u64, 187600'u64), ("00000000", 0'u64), "375199.0000"), ((6003217'u64, 52'u64, "5e945218", 187598'u64, 187600'u64), ("00000000", 0'u64), "375198.0000"), ((6003217'u64, 52'u64, "5e945218", 187597'u64, 187600'u64), ("00000000", 0'u64), "375197.0000"), ((6003217'u64, 52'u64, "5e945218", 187596'u64, 187600'u64), ("00000000", 0'u64), "375196.0000"), ((6003217'u64, 52'u64, "5e945218", 187595'u64, 187600'u64), ("00000000", 0'u64), "375195.0000"), # Attestation score with block monitoring disabled #2. ((6003257'u64, 9'u64, "7bfa464e", 187600'u64, 187601'u64), ("00000000", 0'u64), "375201.0000"), ((6003257'u64, 9'u64, "7bfa464e", 187599'u64, 187601'u64), ("00000000", 0'u64), "375200.0000"), ((6003257'u64, 9'u64, "7bfa464e", 187598'u64, 187601'u64), ("00000000", 0'u64), "375199.0000"), ((6003257'u64, 9'u64, "7bfa464e", 187597'u64, 187601'u64), ("00000000", 0'u64), "375198.0000"), ((6003257'u64, 9'u64, "7bfa464e", 187596'u64, 187601'u64), ("00000000", 0'u64), "375197.0000"), ] proc init(t: typedesc[Eth2Digest], data: string): Eth2Digest = let length = len(data) var dst = Eth2Digest() try: hexToByteArray(data.toOpenArray(0, len(data) - 1), dst.data.toOpenArray(0, (length div 2) - 1)) except ValueError: discard dst proc init*(t: typedesc[ProduceAttestationDataResponse], ad: AttestationDataTuple): ProduceAttestationDataResponse = ProduceAttestationDataResponse(data: AttestationData( slot: Slot(ad.slot), index: ad.index, beacon_block_root: Eth2Digest.init(ad.beacon_block_root), source: Checkpoint(epoch: Epoch(ad.source)), target: Checkpoint(epoch: Epoch(ad.target)) )) proc createRootsSeen( root: tuple[root: string, slot: uint64]): Table[Eth2Digest, Slot] = var res: Table[Eth2Digest, Slot] res[Eth2Digest.init(root.root)] = Slot(root.slot) res suite "Validator Client test suite": test "normalizeUri() test vectors": for hostname in HostNames: for vector in GoodTestVectors: let expect = vector[1] % (hostname) check $normalizeUri(parseUri(vector[0] % (hostname))) == expect test "getAttestationDataScore() test vectors": for vector in AttestationDataVectors: let adata = ProduceAttestationDataResponse.init(vector[0]) roots = createRootsSeen(vector[1]) score = shortScore(roots.getAttestationDataScore(adata)) check score == vector[2] asyncTest "firstSuccessParallel() API timeout test": let uri = parseUri("http://127.0.0.1/") beaconNodes = @[BeaconNodeServerRef.init(uri, 0).tryGet()] vconf = ValidatorClientConf.load( cmdLine = mapIt(["--beacon-node=http://127.0.0.1"], it)) epoch = Epoch(1) strategy = ApiStrategyKind.Priority var gotCancellation = false var vc = ValidatorClientRef(config: vconf, beaconNodes: beaconNodes) vc.fallbackService = await FallbackServiceRef.init(vc) proc getTestDuties(client: RestClientRef, epoch: Epoch): Future[RestPlainResponse] {.async.} = try: await sleepAsync(1.seconds) except CancelledError as exc: gotCancellation = true raise exc const RequestName = "getTestDuties" let response = vc.firstSuccessParallel( RestPlainResponse, uint64, 100.milliseconds, AllBeaconNodeStatuses, {BeaconNodeRole.Duties}, getTestDuties(it, epoch)): check: apiResponse.isErr() apiResponse.error == "Timeout exceeded while awaiting for the response" ApiResponse[uint64].err(apiResponse.error) check: response.isErr() gotCancellation == true asyncTest "bestSuccess() API timeout test": let uri = parseUri("http://127.0.0.1/") beaconNodes = @[BeaconNodeServerRef.init(uri, 0).tryGet()] vconf = ValidatorClientConf.load( cmdLine = mapIt(["--beacon-node=http://127.0.0.1"], it)) epoch = Epoch(1) strategy = ApiStrategyKind.Priority var gotCancellation = false var vc = ValidatorClientRef(config: vconf, beaconNodes: beaconNodes) vc.fallbackService = await FallbackServiceRef.init(vc) proc getTestDuties(client: RestClientRef, epoch: Epoch): Future[RestPlainResponse] {.async.} = try: await sleepAsync(1.seconds) except CancelledError as exc: gotCancellation = true raise exc proc getTestScore(data: uint64): float64 = Inf const RequestName = "getTestDuties" let response = vc.bestSuccess( RestPlainResponse, uint64, 100.milliseconds, AllBeaconNodeStatuses, {BeaconNodeRole.Duties}, getTestDuties(it, epoch), getTestScore(itresponse)): check: apiResponse.isErr() apiResponse.error == "Timeout exceeded while awaiting for the response" ApiResponse[uint64].err(apiResponse.error) check: response.isErr() gotCancellation == true test "getLiveness() response deserialization test": proc generateLivenessResponse(T: typedesc[string], start, count, modv: int): string = var res: seq[string] for index in start ..< (start + count): let validator = Base10.toString(uint64(index)) visibility = if index mod modv == 0: "true" else: "false" res.add("{\"index\":\"" & validator & "\",\"is_live\":" & visibility & "}") "{\"data\":[" & res.join(",") & "]}" proc generateLivenessResponse( T: typedesc[RestLivenessItem], start, count, modv: int ): seq[RestLivenessItem] = var res: seq[RestLivenessItem] for index in start ..< (start + count): let visibility = if index mod modv == 0: true else: false res.add(RestLivenessItem(index: ValidatorIndex(uint64(index)), is_live: visibility)) res const Tests = [(0, 2_000_000, 3)] for test in Tests: let datastr = string.generateLivenessResponse( test[0], test[1], test[2]) data = stringToBytes(datastr) contentType = getContentType("application/json").get() res = decodeBytes(GetValidatorsLivenessResponse, data, Opt.some(contentType)) expect = RestLivenessItem.generateLivenessResponse( test[0], test[1], test[2]) check: res.isOk() res.get().data == expect let vector = stringToBytes( "{\"data\":[{\"index\":\"100000\",\"epoch\":\"202919\",\"is_live\":true}]}") let contentType = getContentType("application/json").tryGet() let res = decodeBytes( GetValidatorsLivenessResponse, vector, Opt.some(contentType)) check: res.isOk() len(res.get().data) == 1 res.get().data[0].index == 100000 res.get().data[0].is_live == true