Spec-compliant implementation of Eth1 monitoring; Eth1-enabled local sim

BEWARE! This commit will trigger a stack overflow during local sim
This commit is contained in:
Zahary Karadjov 2020-03-24 13:13:07 +02:00 committed by zah
parent 39a893ea90
commit 740b76d152
21 changed files with 791 additions and 226 deletions

5
.gitmodules vendored
View File

@ -148,3 +148,8 @@
url = https://github.com/status-im/nim-protobuf-serialization.git url = https://github.com/status-im/nim-protobuf-serialization.git
ignore = dirty ignore = dirty
branch = master branch = master
[submodule "vendor/nim-rocksdb"]
path = vendor/nim-rocksdb
url = https://github.com/status-im/nim-rocksdb.git
ignore = dirty
branch = master

View File

@ -31,6 +31,7 @@ requires "nim >= 0.19.0",
"nimcrypto", "nimcrypto",
"serialization", "serialization",
"stew", "stew",
"testutils",
"prompt", "prompt",
"web3", "web3",
"yaml" "yaml"
@ -45,6 +46,12 @@ proc buildBinary(name: string, srcDir = "./", params = "", cmdParams = "", lang
extra_params &= " " & paramStr(i) extra_params &= " " & paramStr(i)
exec "nim " & lang & " --out:./build/" & name & " -r " & extra_params & " " & srcDir & name & ".nim" & " " & cmdParams exec "nim " & lang & " --out:./build/" & name & " -r " & extra_params & " " & srcDir & name & ".nim" & " " & cmdParams
task moduleTests, "Run all module tests":
buildBinary "beacon_node", "beacon_chain/",
"-d:chronicles_log_level=TRACE " &
"-d:const_preset=minimal " &
"-d:testutils_test_build"
### tasks ### tasks
task test, "Run all tests": task test, "Run all tests":
# We're enabling the TRACE log level so we're sure that those rarely used # We're enabling the TRACE log level so we're sure that those rarely used

View File

@ -168,9 +168,11 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
# Didn't work, try creating a genesis state using main chain monitor # Didn't work, try creating a genesis state using main chain monitor
# TODO Could move this to a separate "GenesisMonitor" process or task # TODO Could move this to a separate "GenesisMonitor" process or task
# that would do only this - see # that would do only this - see
if conf.depositWeb3Url.len != 0: if conf.web3Url.len > 0 and conf.depositContractAddress.len > 0:
mainchainMonitor = MainchainMonitor.init( mainchainMonitor = MainchainMonitor.init(
conf.depositWeb3Url, conf.depositContractAddress, Eth2Digest()) web3Provider(conf.web3Url),
conf.depositContractAddress,
Eth2Digest())
mainchainMonitor.start() mainchainMonitor.start()
else: else:
error "No initial state, need genesis state or deposit contract address" error "No initial state, need genesis state or deposit contract address"
@ -198,9 +200,12 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
let let
blockPool = BlockPool.init(db) blockPool = BlockPool.init(db)
if mainchainMonitor.isNil and conf.depositWeb3Url.len != 0: if mainchainMonitor.isNil and
conf.web3Url.len > 0 and
conf.depositContractAddress.len > 0:
mainchainMonitor = MainchainMonitor.init( mainchainMonitor = MainchainMonitor.init(
conf.depositWeb3Url, conf.depositContractAddress, web3Provider(conf.web3Url),
conf.depositContractAddress,
blockPool.headState.data.data.eth1_data.block_hash) blockPool.headState.data.data.eth1_data.block_hash)
# TODO if we don't have any validators attached, we don't need a mainchain # TODO if we don't have any validators attached, we don't need a mainchain
# monitor # monitor
@ -399,12 +404,10 @@ proc proposeBlock(node: BeaconNode,
node.blockPool.tmpState, head.atSlot(slot)): node.blockPool.tmpState, head.atSlot(slot)):
let (eth1data, deposits) = let (eth1data, deposits) =
if node.mainchainMonitor.isNil: if node.mainchainMonitor.isNil:
(get_eth1data_stub( (get_eth1data_stub(state.eth1_deposit_index, slot.compute_epoch_at_slot()),
state.eth1_deposit_index, slot.compute_epoch_at_slot()), newSeq[Deposit]())
newSeq[Deposit]())
else: else:
(node.mainchainMonitor.eth1Data, node.mainchainMonitor.getBlockProposalData(state)
node.mainchainMonitor.getPendingDeposits())
let message = makeBeaconBlock( let message = makeBeaconBlock(
state, state,
@ -1301,8 +1304,7 @@ when hasPrompt:
render statusBar render statusBar
# p.showPrompt # p.showPrompt
except Exception as e: # render raises Exception except Exception as e: # render raises Exception
if e is Defect: raise (ref Defect)(e) logLoggingFailure(cstring(msg), e)
discard # Status bar not critical
proc statusBarUpdatesPollingLoop() {.async.} = proc statusBarUpdatesPollingLoop() {.async.} =
while true: while true:
@ -1314,7 +1316,7 @@ when hasPrompt:
# var t: Thread[ptr Prompt] # var t: Thread[ptr Prompt]
# createThread(t, processPromptCommands, addr p) # createThread(t, processPromptCommands, addr p)
when isMainModule: programMain:
let let
banner = clientId & "\p" & copyrights & "\p\p" & nimBanner banner = clientId & "\p" & copyrights & "\p\p" & nimBanner
config = BeaconNodeConf.load(version = banner, copyrightBanner = banner) config = BeaconNodeConf.load(version = banner, copyrightBanner = banner)
@ -1324,8 +1326,8 @@ when isMainModule:
proc (logLevel: LogLevel, msg: LogOutputStr) {.gcsafe, raises: [Defect].} = proc (logLevel: LogLevel, msg: LogOutputStr) {.gcsafe, raises: [Defect].} =
try: try:
stdout.write(msg) stdout.write(msg)
except IOError: except IOError as err:
discard # nothing to do.. logLoggingFailure(cstring(msg), err)
randomize() randomize()
@ -1358,8 +1360,8 @@ when isMainModule:
let let
startTime = uint64(times.toUnix(times.getTime()) + config.genesisOffset) startTime = uint64(times.toUnix(times.getTime()) + config.genesisOffset)
outGenesis = config.outputGenesis.string outGenesis = config.outputGenesis.string
eth1Hash = if config.depositWeb3Url.len == 0: eth1BlockHash eth1Hash = if config.web3Url.len == 0: eth1BlockHash
else: waitFor getLatestEth1BlockHash(config.depositWeb3Url) else: waitFor getLatestEth1BlockHash(config.web3Url)
var var
initialState = initialize_beacon_state_from_eth1( initialState = initialize_beacon_state_from_eth1(
eth1Hash, startTime, deposits, {skipBlsValidation, skipMerkleValidation}) eth1Hash, startTime, deposits, {skipBlsValidation, skipMerkleValidation})
@ -1446,16 +1448,26 @@ when isMainModule:
config.totalRandomDeposits, config.depositsDir, true, config.totalRandomDeposits, config.depositsDir, true,
firstIdx = config.totalQuickstartDeposits) firstIdx = config.totalQuickstartDeposits)
if config.depositWeb3Url.len > 0 and config.depositContractAddress.len > 0: if config.web3Url.len > 0 and config.depositContractAddress.len > 0:
if config.minDelay > config.maxDelay:
echo "The minimum delay should not be larger than the maximum delay"
quit 1
var delayGenerator: DelayGenerator
if config.maxDelay > 0.0:
delayGenerator = proc (): chronos.Duration {.gcsafe.} =
chronos.milliseconds (rand(config.minDelay..config.maxDelay)*1000).int
info "Sending deposits", info "Sending deposits",
web3 = config.depositWeb3Url, web3 = config.web3Url,
depositContract = config.depositContractAddress depositContract = config.depositContractAddress
waitFor sendDeposits( waitFor sendDeposits(
quickstartDeposits & randomDeposits, quickstartDeposits & randomDeposits,
config.depositWeb3Url, config.web3Url,
config.depositContractAddress, config.depositContractAddress,
config.depositPrivateKey) config.depositPrivateKey,
delayGenerator)
of query: of query:
case config.queryCmd case config.queryCmd

View File

@ -52,7 +52,7 @@ type
abbr: "d" abbr: "d"
name: "data-dir" }: OutDir name: "data-dir" }: OutDir
depositWeb3Url* {. web3Url* {.
defaultValue: "" defaultValue: ""
desc: "URL of the Web3 server to observe Eth1." desc: "URL of the Web3 server to observe Eth1."
name: "web3-url" }: string name: "web3-url" }: string
@ -252,6 +252,16 @@ type
desc: "Private key of the controlling (sending) account", desc: "Private key of the controlling (sending) account",
name: "deposit-private-key" }: string name: "deposit-private-key" }: string
minDelay* {.
defaultValue: 0.0
desc: "Minimum possible delay between making two deposits (in seconds)"
name: "min-delay" }: float
maxDelay* {.
defaultValue: 0.0
desc: "Maximum possible delay between making two deposits (in seconds)"
name: "max-delay" }: float
of query: of query:
case queryCmd* {. case queryCmd* {.
defaultValue: nimQuery defaultValue: nimQuery

View File

@ -14,7 +14,7 @@ type
sendEth sendEth
CliConfig = object CliConfig = object
depositWeb3Url* {. web3Url* {.
desc: "URL of the Web3 server to observe Eth1" desc: "URL of the Web3 server to observe Eth1"
name: "web3-url" }: string name: "web3-url" }: string
@ -65,7 +65,7 @@ proc sendEth(web3: Web3, to: string, valueEth: int): Future[TxHash] =
proc main() {.async.} = proc main() {.async.} =
let cfg = CliConfig.load() let cfg = CliConfig.load()
let web3 = await newWeb3(cfg.depositWeb3Url) let web3 = await newWeb3(cfg.web3Url)
if cfg.privateKey.len != 0: if cfg.privateKey.len != 0:
web3.privateKey = PrivateKey.fromHex(cfg.privateKey)[] web3.privateKey = PrivateKey.fromHex(cfg.privateKey)[]
else: else:

View File

@ -9,7 +9,7 @@ import
json_serialization, json_serialization/std/[net, options], json_serialization, json_serialization/std/[net, options],
chronos, chronicles, metrics, chronos, chronicles, metrics,
# TODO: create simpler to use libp2p modules that use re-exports # TODO: create simpler to use libp2p modules that use re-exports
libp2p/[switch, standard_setup, peerinfo, peer, connection, libp2p/[switch, standard_setup, peerinfo, peer, connection, errors,
multiaddress, multicodec, crypto/crypto, crypto/secp, multiaddress, multicodec, crypto/crypto, crypto/secp,
protocols/identify, protocols/protocol], protocols/identify, protocols/protocol],
libp2p/protocols/secure/[secure, secio], libp2p/protocols/secure/[secure, secio],
@ -39,6 +39,10 @@ type
Bytes = seq[byte] Bytes = seq[byte]
# TODO: This is here only to eradicate a compiler
# warning about unused import (rpc/messages).
GossipMsg = messages.Message
# TODO Is this really needed? # TODO Is this really needed?
Eth2Node* = ref object of RootObj Eth2Node* = ref object of RootObj
switch*: Switch switch*: Switch
@ -47,6 +51,7 @@ type
peerPool*: PeerPool[Peer, PeerID] peerPool*: PeerPool[Peer, PeerID]
protocolStates*: seq[RootRef] protocolStates*: seq[RootRef]
libp2pTransportLoops*: seq[Future[void]] libp2pTransportLoops*: seq[Future[void]]
discoveryLoop: Future[void]
metadata*: Eth2Metadata metadata*: Eth2Metadata
EthereumNode = Eth2Node # needed for the definitions in p2p_backends_helpers EthereumNode = Eth2Node # needed for the definitions in p2p_backends_helpers
@ -511,7 +516,7 @@ proc performProtocolHandshakes*(peer: Peer) {.async.} =
if protocol.handshake != nil: if protocol.handshake != nil:
subProtocolsHandshakes.add((protocol.handshake)(peer, nil)) subProtocolsHandshakes.add((protocol.handshake)(peer, nil))
await all(subProtocolsHandshakes) await allFuturesThrowing(subProtocolsHandshakes)
template initializeConnection*(peer: Peer): auto = template initializeConnection*(peer: Peer): auto =
performProtocolHandshakes(peer) performProtocolHandshakes(peer)
@ -755,7 +760,8 @@ proc start*(node: Eth2Node) {.async.} =
node.discovery.open() node.discovery.open()
node.discovery.start() node.discovery.start()
node.libp2pTransportLoops = await node.switch.start() node.libp2pTransportLoops = await node.switch.start()
traceAsyncErrors node.runDiscoveryLoop() node.discoveryLoop = node.runDiscoveryLoop()
traceAsyncErrors node.discoveryLoop
proc init*(T: type Peer, network: Eth2Node, info: PeerInfo): Peer = proc init*(T: type Peer, network: Eth2Node, info: PeerInfo): Peer =
new result new result
@ -1032,9 +1038,11 @@ proc subscribe*[MsgType](node: Eth2Node,
msgValidator SSZ.decode(gossipBytes, MsgType) msgValidator SSZ.decode(gossipBytes, MsgType)
# Validate messages as soon as subscribed # Validate messages as soon as subscribed
let incomingMsgValidator = proc(topic: string, message: messages.Message): let incomingMsgValidator = proc(topic: string,
Future[bool] {.async, gcsafe.} = message: GossipMsg): Future[bool]
{.async, gcsafe.} =
return execMsgValidator(message.data, topic) return execMsgValidator(message.data, topic)
node.switch.addValidator(topic, incomingMsgValidator) node.switch.addValidator(topic, incomingMsgValidator)
let incomingMsgHandler = proc(topic: string, let incomingMsgHandler = proc(topic: string,

View File

@ -1,105 +1,430 @@
import import
deques, tables, hashes, options,
chronos, web3, json, chronicles, chronos, web3, json, chronicles,
spec/[datatypes, digest, crypto, beaconstate, helpers] spec/[datatypes, digest, crypto, beaconstate, helpers]
contract(DepositContract):
proc deposit(pubkey: Bytes48,
withdrawalCredentials: Bytes32,
signature: Bytes96,
deposit_data_root: FixedBytes[32])
proc get_deposit_root(): FixedBytes[32]
proc get_deposit_count(): Bytes8
proc DepositEvent(pubkey: Bytes48,
withdrawalCredentials: Bytes32,
amount: Bytes8,
signature: Bytes96,
index: Bytes8) {.event.}
# TODO
# The raises list of this module are still not usable due to general
# Exceptions being reported from Chronos's asyncfutures2.
type type
Eth1BlockNumber* = uint64
Eth1BlockTimestamp* = uint64
Eth1Block* = ref object
number*: Eth1BlockNumber
timestamp*: Eth1BlockTimestamp
deposits*: seq[Deposit]
voteData*: Eth1Data
Eth1Chain* = object
blocks: Deque[Eth1Block]
blocksByHash: Table[BlockHash, Eth1Block]
MainchainMonitor* = ref object MainchainMonitor* = ref object
web3Url: string startBlock: BlockHash
depositContractAddress: Address depositContractAddress: Address
dataProviderFactory*: DataProviderFactory
genesisState: ref BeaconState genesisState: ref BeaconState
genesisStateFut: Future[void] genesisStateFut: Future[void]
pendingDeposits: seq[Deposit] eth1Chain: Eth1Chain
depositCount: uint64
curBlock: uint64
depositQueue: AsyncQueue[QueueElement]
eth1Block: BlockHash
eth1Data*: Eth1Data
depositQueue: AsyncQueue[DepositQueueElem]
runFut: Future[void] runFut: Future[void]
QueueElement = (BlockHash, DepositData) Web3EventType = enum
NewEvent
RemovedEvent
proc init*( DepositQueueElem = (BlockHash, Web3EventType)
T: type MainchainMonitor,
web3Url, depositContractAddress: string,
startBlock: Eth2Digest): T =
T(
web3Url: web3Url,
depositContractAddress: Address.fromHex(depositContractAddress),
depositQueue: newAsyncQueue[QueueElement](),
eth1Block: BlockHash(startBlock.data),
)
contract(DepositContract): DataProvider* = object of RootObj
proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32]) DataProviderRef* = ref DataProvider
proc get_deposit_root(): FixedBytes[32]
proc get_deposit_count(): Bytes8 DataProviderFactory* = object
proc DepositEvent(pubkey: Bytes48, withdrawalCredentials: Bytes32, amount: Bytes8, signature: Bytes96, index: Bytes8) {.event.} desc: string
new: proc(depositContractAddress: Address): Future[DataProviderRef] {.
gcsafe
# raises: [Defect]
.}
Web3DataProvider* = object of DataProvider
url: string
web3: Web3
ns: Sender[DepositContract]
subscription: Subscription
Web3DataProviderRef* = ref Web3DataProvider
ReorgDepthLimitExceeded = object of CatchableError
CorruptDataProvider = object of CatchableError
DisconnectHandler* = proc () {.gcsafe, raises: [Defect].}
DepositEventHandler* = proc (
pubkey: Bytes48,
withdrawalCredentials: Bytes32,
amount: Bytes8,
signature: Bytes96, merkleTreeIndex: Bytes8, j: JsonNode) {.gcsafe.}
const
reorgDepthLimit = 1000
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data
func compute_time_at_slot(state: BeaconState, slot: Slot): uint64 =
return state.genesis_time + slot * SECONDS_PER_SLOT
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data
func voting_period_start_time*(state: BeaconState): uint64 =
let eth1_voting_period_start_slot = state.slot - state.slot mod SLOTS_PER_ETH1_VOTING_PERIOD.uint64
return compute_time_at_slot(state, eth1_voting_period_start_slot)
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data
func is_candidate_block(blk: Eth1Block, period_start: uint64): bool =
(blk.timestamp <= period_start - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE) and
(blk.timestamp >= period_start - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2)
func asEth2Digest(x: BlockHash): Eth2Digest =
Eth2Digest(data: array[32, byte](x))
template asBlockHash(x: Eth2Digest): BlockHash =
BlockHash(x.data)
func getDepositsInRange(eth1Chain: Eth1Chain,
sinceBlock, latestBlock: Eth1BlockNumber): seq[Deposit] =
## Returns all deposits that happened AFTER the block `sinceBlock` (not inclusive).
## The deposits in `latestBlock` will be included.
if latestBlock <= sinceBlock: return
let firstBlockInCache = eth1Chain.blocks[0].number
# This function should be used with indices obtained with `eth1Chain.findBlock`.
# This guarantess that both of these indices will be valid:
doAssert sinceBlock >= firstBlockInCache and
int(latestBlock - firstBlockInCache) < eth1Chain.blocks.len
let
sinceBlockIdx = sinceBlock - firstBlockInCache
latestBlockIdx = latestBlock - firstBlockInCache
for i in (sinceBlockIdx + 1) ..< latestBlockIdx:
result.add eth1Chain.blocks[i].deposits
template findBlock*(eth1Chain: Eth1Chain, hash: BlockHash): Eth1Block =
eth1Chain.blocksByHash.getOrDefault(hash, nil)
template findBlock*(eth1Chain: Eth1Chain, eth1Data: Eth1Data): Eth1Block =
getOrDefault(eth1Chain.blocksByHash, asBlockHash(eth1Data.block_hash), nil)
proc findParent*(eth1Chain: Eth1Chain, blk: BlockObject): Eth1Block =
result = eth1Chain.findBlock(blk.parentHash)
# a distinct type is stipped here:
let blockNumber = Eth1BlockNumber(blk.number)
if result != nil and result.number != blockNumber - 1:
debug "Found inconsistent numbering of Eth1 blocks. Ignoring block.",
blockHash = blk.hash.toHex, blockNumber,
parentHash = blk.parentHash.toHex, parentNumber = result.number
result = nil
when false:
func getCacheIdx(eth1Chain: Eth1Chain, blockNumber: Eth1BlockNumber): int =
if eth1Chain.blocks.len == 0:
return -1
let idx = blockNumber - eth1Chain.blocks[0].number
if idx < 0 or idx >= eth1Chain.blocks.len:
return -1
idx
func `{}`*(eth1Chain: Eth1Chain, blockNumber: Eth1BlockNumber): Eth1Block =
## Finds a block in our cache that corresponds to a particular Eth block
## number. May return `nil` if we don't have such a block in the cache.
let idx = eth1Chain.getCacheIdx(blockNumber)
if idx != -1: eth1Chain.blocks[idx] else: nil
func latestCandidateBlock(eth1Chain: Eth1Chain, periodStart: uint64): Eth1Block =
for i in countdown(eth1Chain.blocks.len - 1, 0):
let blk = eth1Chain.blocks[i]
if is_candidate_block(blk, periodStart):
return blk
func trimHeight(eth1Chain: var Eth1Chain, blockNumber: Eth1BlockNumber) =
## Removes all blocks above certain `blockNumber`
if eth1Chain.blocks.len == 0:
return
let newLen = max(0, int(blockNumber - eth1Chain.blocks[0].number + 1))
for i in newLen ..< eth1Chain.blocks.len:
let removed = eth1Chain.blocks.popLast
eth1Chain.blocksByHash.del removed.voteData.block_hash.asBlockHash
template purgeChain*(eth1Chain: var Eth1Chain, blk: Eth1Block) =
## This is used when we discover that a previously considered block
## is no longer part of the selected chain (due to a reorg). We can
## then remove from our chain together with all blocks that follow it.
trimHeight(eth1Chain, blk.number - 1)
func purgeChain*(eth1Chain: var Eth1Chain, blockHash: BlockHash) =
let blk = eth1Chain.findBlock(blockHash)
if blk != nil: eth1Chain.purgeChain(blk)
template purgeDescendants*(eth1CHain: Eth1Chain, blk: Eth1Block) =
trimHeight(eth1Chain, blk.number)
func addBlock*(eth1Chain: var Eth1Chain, newBlock: Eth1Block) =
if eth1Chain.blocks.len > 0:
doAssert eth1Chain.blocks.peekLast.number + 1 == newBlock.number
eth1Chain.blocks.addLast newBlock
eth1Chain.blocksByHash[newBlock.voteData.block_hash.asBlockHash] = newBlock
func totalDeposits*(eth1Chain: Eth1Chain): int =
for blk in eth1Chain.blocks:
result += blk.deposits.len
func allDeposits*(eth1Chain: Eth1Chain): seq[Deposit] =
for blk in eth1Chain.blocks:
result.add blk.deposits
template hash*(x: Eth1Block): Hash =
hash(x.voteData.block_hash.data)
template notImplemented =
doAssert false, "Method not implemented"
method getBlockByHash*(p: DataProviderRef, hash: BlockHash): Future[BlockObject] {.
base
gcsafe
locks: 0
# raises: [Defect]
.} =
discard
# notImplemented
method onDisconnect*(p: DataProviderRef, handler: DisconnectHandler) {.
base
gcsafe
locks: 0
# raises: []
.} =
notImplemented
method onDepositEvent*(p: DataProviderRef,
startBlock: Eth1BlockNumber,
handler: DepositEventHandler): Future[void] {.
base
gcsafe
locks: 0
# raises: []
.} =
notImplemented
method close*(p: DataProviderRef): Future[void] {.
base
gcsafe
locks: 0
# raises: [Defect]
.} =
notImplemented
method fetchDepositData*(p: DataProviderRef,
web3Block: BlockObject): Future[Eth1Block] {.
base
gcsafe
locks: 0
# raises: [Defect, CatchableError]
.} =
notImplemented
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data
func getBlockProposalData*(eth1Chain: Eth1Chain,
state: BeaconState): (Eth1Data, seq[Deposit]) =
template voteForNoChange() =
return (state.eth1_data, newSeq[Deposit]())
let prevBlock = eth1Chain.findBlock(state.eth1_data)
if prevBlock == nil:
# The Eth1 block currently referenced in the BeaconState is unknown to us.
# This situation is not specifically covered in the honest validator spec,
# but there is a similar condition where none of the eth1_data_votes is
# present in our worldview. The suggestion there is to vote for "no change"
# and we'll do the same here:
voteForNoChange()
let periodStart = voting_period_start_time(state)
var otherVotesCountTable = initCountTable[Eth1Block]()
for vote in state.eth1_data_votes:
let eth1Block = eth1Chain.findBlock(vote)
if eth1Block != nil and is_candidate_block(eth1Block, periodStart):
otherVotesCountTable.inc eth1Block
var ourVote: Eth1Block
if otherVotesCountTable.len > 0:
ourVote = otherVotesCountTable.largest.key
else:
ourVote = eth1Chain.latestCandidateBlock(periodStart)
if ourVote == nil:
voteForNoChange()
(ourVote.voteData, eth1Chain.getDepositsInRange(prevBlock.number, ourVote.number))
template getBlockProposalData*(m: MainchainMonitor, state: BeaconState): untyped =
getBlockProposalData(m.eth1Chain, state)
proc init*(T: type MainchainMonitor,
dataProviderFactory: DataProviderFactory,
depositContractAddress: string,
startBlock: Eth2Digest): T =
T(depositContractAddress: Address.fromHex(depositContractAddress),
depositQueue: newAsyncQueue[DepositQueueElem](),
startBlock: BlockHash(startBlock.data),
dataProviderFactory: dataProviderFactory)
const MIN_GENESIS_TIME = 0 const MIN_GENESIS_TIME = 0
proc updateEth1Data(m: MainchainMonitor, count: uint64, root: FixedBytes[32]) = proc readJsonDeposits(json: JsonNode): seq[Deposit] =
m.eth1Data.deposit_count = count if json.kind != JArray:
m.eth1Data.deposit_root.data = array[32, byte](root) raise newException(CatchableError,
m.eth1Data.block_hash.data = array[32, byte](m.eth1Block) "Web3 provider didn't return a list of deposit events")
proc processDeposits(m: MainchainMonitor, web3: Web3) {.async.} = for logEvent in json:
var logData = strip0xPrefix(json["data"].getStr)
var
pubkey: Bytes48
withdrawalCredentials: Bytes32
amount: Bytes8
signature: Bytes96
index: Bytes8
var offset = 0
offset = decode(logData, offset, pubkey)
offset = decode(logData, offset, withdrawalCredentials)
offset = decode(logData, offset, amount)
offset = decode(logData, offset, signature)
offset = decode(logData, offset, index)
result.add Deposit(
# proof: TODO
data: DepositData(
pubkey: ValidatorPubKey.init(array[48, byte](pubkey)),
withdrawal_credentials: Eth2Digest(data: array[32, byte](withdrawalCredentials)),
amount: bytes_to_int(array[8, byte](amount)),
signature: ValidatorSig.init(array[96, byte](signature))))
proc checkForGenesisEvent(m: MainchainMonitor) =
if not m.genesisState.isNil:
return
let lastBlock = m.eth1Chain.blocks.peekLast
const totalDepositsNeeded = max(SLOTS_PER_EPOCH,
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT)
if lastBlock.timestamp.uint64 >= MIN_GENESIS_TIME.uint64 and
m.eth1Chain.totalDeposits >= totalDepositsNeeded:
# This block is a genesis candidate
let startTime = lastBlock.timestamp.uint64
var s = initialize_beacon_state_from_eth1(lastBlock.voteData.block_hash,
startTime, m.eth1Chain.allDeposits, {})
if is_valid_genesis_state(s):
# https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start#create-genesis-state
s.genesis_time = startTime
m.genesisState.new()
m.genesisState[] = s
if not m.genesisStateFut.isNil:
m.genesisStateFut.complete()
m.genesisStateFut = nil
proc processDeposits(m: MainchainMonitor, dataProvider: DataProviderRef) {.
async
# raises: [Defect]
.} =
# ATTENTION!
# Please note that this code is using a queue to guarantee the
# strict serial order of processing of deposits. If we had the
# same code embedded in the deposit contracts events handler,
# it could easily re-order the steps due to the intruptable
# interleaved execution of async code.
while true: while true:
let (blkHash, data) = await m.depositQueue.popFirst() let (blockHash, eventType) = await m.depositQueue.popFirst()
var blk: BlockObject
var depositCount: uint64
var depositRoot: FixedBytes[32]
try:
blk = await web3.provider.eth_getBlockByHash(blkHash, false)
let ns = web3.contractSender(DepositContract, m.depositContractAddress) if eventType == RemovedEvent:
m.eth1Chain.purgeChain(blockHash)
continue
# TODO: use m.eth1Block for web3 calls let cachedBlock = m.eth1Chain.findBlock(blockHash)
let cnt = await ns.get_deposit_count().call() if cachedBlock == nil:
depositRoot = await ns.get_deposit_root().call() try:
depositCount = bytes_to_int(array[8, byte](cnt)) let
web3Block = await dataProvider.getBlockByHash(blockHash)
eth1Block = await dataProvider.fetchDepositData(web3Block)
except: if m.eth1Chain.blocks.len > 0:
# Connection problem? Put the unprocessed deposit back to queue var cachedParent = m.eth1Chain.findParent(web3Block)
m.depositQueue.addFirstNoWait((blkHash, data)) if cachedParent == nil:
raise # We are missing the parent block.
# This shouldn't be happening if the deposits events are reported in
# proper order, but nevertheless let's try to repair our chain:
var chainOfParents = newSeq[Eth1Block]()
var parentHash = web3Block.parentHash
var expectedParentBlockNumber = web3Block.number.uint64 - 1
warn "Eth1 parent block missing. Attempting to request from the network",
parentHash = parentHash.toHex
debug "Got deposit from eth1", pubKey = data.pubKey while true:
if chainOfParents.len > reorgDepthLimit:
error "Detected Eth1 re-org exceeded the maximum depth limit",
headBlockHash = web3Block.hash.toHex,
ourHeadHash = m.eth1Chain.blocks.peekLast.voteData.block_hash
raise newException(ReorgDepthLimitExceeded, "Reorg depth limit exceeded")
let dep = datatypes.Deposit(data: data) let parentWeb3Block = await dataProvider.getBlockByHash(parentHash)
m.pendingDeposits.add(dep) if parentWeb3Block.number.uint64 != expectedParentBlockNumber:
inc m.depositCount error "Eth1 data provider supplied invalid parent block",
m.eth1Block = blkHash parentBlockNumber = parentWeb3Block.number.uint64,
expectedParentBlockNumber, parentHash = parentHash.toHex
raise newException(CorruptDataProvider,
"Parent block with incorrect number")
if m.pendingDeposits.len >= SLOTS_PER_EPOCH and chainOfParents.add(await dataProvider.fetchDepositData(parentWeb3Block))
m.pendingDeposits.len >= MIN_GENESIS_ACTIVE_VALIDATOR_COUNT and let localParent = m.eth1Chain.findParent(parentWeb3Block)
blk.timestamp.uint64 >= MIN_GENESIS_TIME.uint64: if localParent != nil:
# This block is a genesis candidate m.eth1Chain.purgeDescendants(localParent)
var h: Eth2Digest for i in countdown(chainOfParents.len - 1, 0):
h.data = array[32, byte](blkHash) m.eth1Chain.addBlock chainOfParents[i]
let startTime = blk.timestamp.uint64 cachedParent = m.eth1Chain.blocks.peekLast
var s = initialize_beacon_state_from_eth1( break
h, startTime, m.pendingDeposits, {})
if is_valid_genesis_state(s): dec expectedParentBlockNumber
# https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start#create-genesis-state parentHash = parentWeb3Block.parentHash
s.genesis_time = startTime
m.pendingDeposits.setLen(0) m.eth1Chain.purgeDescendants(cachedParent)
m.genesisState.new()
m.genesisState[] = s
if not m.genesisStateFut.isNil:
m.genesisStateFut.complete()
m.genesisStateFut = nil
# TODO: Set curBlock to blk number
# TODO: This should be progressing in more independent way. m.eth1Chain.addBlock eth1Block
# The Eth1 cross-link can advance even when there are no new deposits. m.checkForGenesisEvent()
m.updateEth1Data(depositCount, depositRoot)
except CatchableError:
# Connection problem? Put the unprocessed deposit back to queue.
# Raising the exception here will lead to a restart of the whole monitor.
m.depositQueue.addFirstNoWait((blockHash, eventType))
raise
proc isRunning*(m: MainchainMonitor): bool = proc isRunning*(m: MainchainMonitor): bool =
not m.runFut.isNil not m.runFut.isNil
@ -114,76 +439,141 @@ proc getGenesis*(m: MainchainMonitor): Future[BeaconState] {.async.} =
doAssert(not m.genesisState.isNil) doAssert(not m.genesisState.isNil)
return m.genesisState[] return m.genesisState[]
proc getBlockNumber(web3: Web3, hash: BlockHash): Future[Quantity] {.async.} = method getBlockByHash*(p: Web3DataProviderRef, hash: BlockHash): Future[BlockObject] =
discard
# p.web3.provider.eth_getBlockByHash(hash, false)
method close*(p: Web3DataProviderRef): Future[void] {.async, locks: 0.} =
if p.subscription != nil:
await p.subscription.unsubscribe()
await p.web3.close()
method fetchDepositData*(p: Web3DataProviderRef,
web3Block: BlockObject): Future[Eth1Block] {.async, locks: 0.} =
let
blockHash = web3Block.hash
depositRoot = await p.ns.get_deposit_root.call(blockNumber = web3Block.number.uint64)
rawCount = await p.ns.get_deposit_count.call(blockNumber = web3Block.number.uint64)
depositCount = bytes_to_int(array[8, byte](rawCount))
depositsJson = await p.ns.getJsonLogs(DepositEvent, blockHash = some(blockHash))
deposits = readJsonDeposits(depositsJson)
return Eth1Block(
number: Eth1BlockNumber(web3Block.number),
timestamp: Eth1BlockTimestamp(web3Block.timestamp),
deposits: deposits,
voteData: Eth1Data(deposit_root: depositRoot.asEth2Digest,
deposit_count: depositCount,
block_hash: blockHash.asEth2Digest))
method onDisconnect*(p: Web3DataProviderRef, handler: DisconnectHandler) {.
gcsafe
locks: 0
# raises: []
.} =
p.web3.onDisconnect = handler
method onDepositEvent*(p: Web3DataProviderRef,
startBlock: Eth1BlockNumber,
handler: DepositEventHandler): Future[void] {.
async
gcsafe
locks: 0
# raises: []
.} =
if p.subscription != nil:
await p.subscription.unsubscribe()
p.subscription = await p.ns.subscribe(
DepositEvent, %*{"fromBlock": startBlock}, handler)
proc getBlockNumber(p: DataProviderRef, hash: BlockHash): Future[Quantity] {.async.} =
debug "Querying block number", hash = $hash debug "Querying block number", hash = $hash
try: try:
let blk = await web3.provider.eth_getBlockByHash(hash, false) let blk = await p.getBlockByHash(hash)
return blk.number return blk.number
except CatchableError as exc: except CatchableError as exc:
# TODO this doesn't make too much sense really, but what would be a notice "Failed to get Eth1 block number from hash",
# reasonable behavior? no idea - the whole algorithm needs to be
# rewritten to match the spec.
notice "Failed to get block number from hash, using current block instead",
hash = $hash, err = exc.msg hash = $hash, err = exc.msg
return await web3.provider.eth_blockNumber() raise
proc new*(T: type Web3DataProvider,
web3Url: string,
depositContractAddress: Address): Future[ref Web3DataProvider] {.
async
# raises: [Defect]
.} =
try:
type R = ref T
let
web3 = await newWeb3(web3Url)
ns = web3.contractSender(DepositContract, depositContractAddress)
return R(url: web3Url, web3: web3, ns: ns)
except CatchableError:
return nil
func web3Provider*(web3Url: string): DataProviderFactory =
proc factory(depositContractAddress: Address): Future[DataProviderRef] {.async.} =
result = await Web3DataProvider.new(web3Url, depositContractAddress)
DataProviderFactory(desc: "web3(" & web3Url & ")", new: factory)
proc run(m: MainchainMonitor, delayBeforeStart: Duration) {.async.} = proc run(m: MainchainMonitor, delayBeforeStart: Duration) {.async.} =
if delayBeforeStart != ZeroDuration: if delayBeforeStart != ZeroDuration:
await sleepAsync(delayBeforeStart) await sleepAsync(delayBeforeStart)
let web3 = await newWeb3(m.web3Url) let dataProvider = await m.dataProviderFactory.new(m.depositContractAddress)
defer: await web3.close() if dataProvider == nil:
error "Failed to initialize Eth1 data provider",
provider = m.dataProviderFactory.desc
raise newException(CatchableError, "Failed to initialize Eth1 data provider")
defer: await close(dataProvider)
let processFut = m.processDeposits(web3) let processFut = m.processDeposits(dataProvider)
defer: await processFut
web3.onDisconnect = proc() = dataProvider.onDisconnect do:
error "Web3 server disconnected", ulr = m.web3Url error "Eth1 data provider disconnected",
provider = m.dataProviderFactory.desc
processFut.cancel() processFut.cancel()
# TODO this needs to implement follow distance and the rest of the honest let startBlkNum = await dataProvider.getBlockNumber(m.startBlock)
# validator spec..
let startBlkNum = await web3.getBlockNumber(m.eth1Block)
notice "Monitoring eth1 deposits", notice "Monitoring eth1 deposits",
fromBlock = startBlkNum.uint64, fromBlock = startBlkNum.uint64,
contract = $m.depositContractAddress, contract = $m.depositContractAddress,
url = m.web3Url url = m.dataProviderFactory.desc
let ns = web3.contractSender(DepositContract, m.depositContractAddress) await dataProvider.onDepositEvent(Eth1BlockNumber(startBlkNum)) do (
let s = await ns.subscribe(DepositEvent, %*{"fromBlock": startBlkNum}) do(
pubkey: Bytes48, pubkey: Bytes48,
withdrawalCredentials: Bytes32, withdrawalCredentials: Bytes32,
amount: Bytes8, amount: Bytes8,
signature: Bytes96, merkleTreeIndex: Bytes8, j: JsonNode): signature: Bytes96, merkleTreeIndex: Bytes8, j: JsonNode):
try: try:
let blkHash = BlockHash.fromHex(j["blockHash"].getStr()) let
let amount = bytes_to_int(array[8, byte](amount)) blockHash = BlockHash.fromHex(j["blockHash"].getStr())
eventType = if j.hasKey("removed"): RemovedEvent
else: NewEvent
m.depositQueue.addLastNoWait((blockHash, eventType))
m.depositQueue.addLastNoWait((blkHash,
DepositData(pubkey: ValidatorPubKey.init(array[48, byte](pubkey)),
withdrawal_credentials: Eth2Digest(data: array[32, byte](withdrawalCredentials)),
amount: amount,
signature: ValidatorSig.init(array[96, byte](signature)))))
except CatchableError as exc: except CatchableError as exc:
warn "Received invalid deposit", err = exc.msg, j warn "Received invalid deposit", err = exc.msg, j
try:
await processFut
finally:
await s.unsubscribe()
proc start(m: MainchainMonitor, delayBeforeStart: Duration) = proc start(m: MainchainMonitor, delayBeforeStart: Duration) =
if m.runFut.isNil: if m.runFut.isNil:
let runFut = m.run(delayBeforeStart) let runFut = m.run(delayBeforeStart)
m.runFut = runFut m.runFut = runFut
runFut.addCallback() do(p: pointer): runFut.addCallback do (p: pointer):
if runFut.failed and runFut == m.runFut: if runFut.failed:
error "Mainchain monitor failure, restarting", err = runFut.error.msg if runFut.error[] of CatchableError:
m.runFut = nil if runFut == m.runFut:
m.start(5.seconds) error "Mainchain monitor failure, restarting", err = runFut.error.msg
m.runFut = nil
m.start(5.seconds)
else:
fatal "Fatal exception reached", err = runFut.error.msg
quit 1
proc start*(m: MainchainMonitor) {.inline.} = proc start*(m: MainchainMonitor) {.inline.} =
m.start(0.seconds) m.start(0.seconds)
@ -193,20 +583,9 @@ proc stop*(m: MainchainMonitor) =
m.runFut.cancel() m.runFut.cancel()
m.runFut = nil m.runFut = nil
proc getPendingDeposits*(m: MainchainMonitor): seq[Deposit] =
# This should be a simple accessor for the reference kept above
m.pendingDeposits
# TODO update after spec change removed Specials
# iterator getValidatorActions*(m: MainchainMonitor,
# fromBlock, toBlock: Eth2Digest): SpecialRecord =
# # It's probably better if this doesn't return a SpecialRecord, but
# # rather a more readable description of the change that can be packed
# # in a SpecialRecord by the client of the API.
# discard
proc getLatestEth1BlockHash*(url: string): Future[Eth2Digest] {.async.} = proc getLatestEth1BlockHash*(url: string): Future[Eth2Digest] {.async.} =
let web3 = await newWeb3(url) let web3 = await newWeb3(url)
defer: await web3.close()
let blk = await web3.provider.eth_getBlockByNumber("latest", false) let blk = await web3.provider.eth_getBlockByNumber("latest", false)
result.data = array[32, byte](blk.hash) return Eth2Digest(data: array[32, byte](blk.hash))
await web3.close()

View File

@ -80,6 +80,8 @@ const
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#configuration # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#configuration
ATTESTATION_PROPAGATION_SLOT_RANGE* = 32 ATTESTATION_PROPAGATION_SLOT_RANGE* = 32
SLOTS_PER_ETH1_VOTING_PERIOD* = Slot(EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH)
template maxSize*(n: int) {.pragma.} template maxSize*(n: int) {.pragma.}
type type

View File

@ -133,8 +133,7 @@ proc process_randao(
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#eth1-data # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#eth1-data
func process_eth1_data(state: var BeaconState, body: BeaconBlockBody) {.nbench.}= func process_eth1_data(state: var BeaconState, body: BeaconBlockBody) {.nbench.}=
state.eth1_data_votes.add body.eth1_data state.eth1_data_votes.add body.eth1_data
if state.eth1_data_votes.count(body.eth1_data) * 2 > if state.eth1_data_votes.count(body.eth1_data) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD.int:
EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH:
state.eth1_data = body.eth1_data state.eth1_data = body.eth1_data
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#is_slashable_validator # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#is_slashable_validator

View File

@ -235,3 +235,4 @@ func is_proposer(
var cache = get_empty_per_epoch_cache() var cache = get_empty_per_epoch_cache()
let proposer_index = get_beacon_proposer_index(state, cache) let proposer_index = get_beacon_proposer_index(state, cache)
proposer_index.isSome and proposer_index.get == validator_index proposer_index.isSome and proposer_index.get == validator_index

View File

@ -7,6 +7,9 @@ import
contract(DepositContract): contract(DepositContract):
proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32]) proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32])
type
DelayGenerator* = proc(): chronos.Duration {.closure, gcsafe.}
proc writeTextFile(filename: string, contents: string) = proc writeTextFile(filename: string, contents: string) =
writeFile(filename, contents) writeFile(filename, contents)
# echo "Wrote ", filename # echo "Wrote ", filename
@ -62,15 +65,16 @@ proc generateDeposits*(totalValidators: int,
proc sendDeposits*( proc sendDeposits*(
deposits: seq[Deposit], deposits: seq[Deposit],
depositWeb3Url, depositContractAddress, privateKey: string) {.async.} = web3Url, depositContractAddress, privateKey: string,
delayGenerator: DelayGenerator = nil) {.async.} =
var web3 = await newWeb3(depositWeb3Url) var web3 = await newWeb3(web3Url)
if privateKey.len != 0: if privateKey.len != 0:
web3.privateKey = PrivateKey.fromHex(privateKey).tryGet() web3.privateKey = PrivateKey.fromHex(privateKey).tryGet()
else: else:
let accounts = await web3.provider.eth_accounts() let accounts = await web3.provider.eth_accounts()
if accounts.len == 0: if accounts.len == 0:
error "No account offered by the web3 provider", web3url = depositWeb3Url error "No account offered by the web3 provider", web3Url
return return
web3.defaultAccount = accounts[0] web3.defaultAccount = accounts[0]
@ -84,17 +88,20 @@ proc sendDeposits*(
Bytes96(dp.data.signature.toRaw()), Bytes96(dp.data.signature.toRaw()),
FixedBytes[32](hash_tree_root(dp.data).data)).send(value = 32.u256.ethToWei, gasPrice = 1) FixedBytes[32](hash_tree_root(dp.data).data)).send(value = 32.u256.ethToWei, gasPrice = 1)
if delayGenerator != nil:
await sleepAsync(delayGenerator())
when isMainModule: when isMainModule:
import confutils import confutils
cli do (totalValidators: int = 125000, cli do (totalValidators: int = 125000,
outputDir: string = "validators", outputDir: string = "validators",
randomKeys: bool = false, randomKeys: bool = false,
depositWeb3Url: string = "", web3Url: string = "",
depositContractAddress: string = ""): depositContractAddress: string = ""):
let deposits = generateDeposits(totalValidators, outputDir, randomKeys) let deposits = generateDeposits(totalValidators, outputDir, randomKeys)
if depositWeb3Url.len() > 0 and depositContractAddress.len() > 0: if web3Url.len() > 0 and depositContractAddress.len() > 0:
echo "Sending deposits to eth1..." echo "Sending deposits to eth1..."
waitFor sendDeposits(deposits, depositWeb3Url, depositContractAddress, "") waitFor sendDeposits(deposits, web3Url, depositContractAddress, "")
echo "Done" echo "Done"

View File

@ -41,6 +41,8 @@ else:
# for heap-usage-by-instance-type metrics and object base-type strings # for heap-usage-by-instance-type metrics and object base-type strings
--define:nimTypeNames --define:nimTypeNames
switch("import", "testutils/moduletests")
# the default open files limit is too low on macOS (512), breaking the # the default open files limit is too low on macOS (512), breaking the
# "--debugger:native" build. It can be increased with `ulimit -n 1024`. # "--debugger:native" build. It can be increased with `ulimit -n 1024`.
if not defined(macosx): if not defined(macosx):

View File

@ -30,7 +30,7 @@ type
exit: SignedVoluntaryExit exit: SignedVoluntaryExit
# This and AssertionError are raised to indicate programming bugs # This and AssertionError are raised to indicate programming bugs
# A wrapper to allow exception tracking to identify unexpected exceptions # A wrapper to allow exception tracking to identify unexpected exceptions
FuzzCrashError = object of Exception FuzzCrashError = object of CatchableError
# TODO: change ptr uint to ptr csize_t when available in newer Nim version. # TODO: change ptr uint to ptr csize_t when available in newer Nim version.
proc copyState(state: BeaconState, output: ptr byte, proc copyState(state: BeaconState, output: ptr byte,

View File

@ -140,5 +140,6 @@ cli do (skipGoerliKey {.
--web3-url={web3Url} --web3-url={web3Url}
{bootstrapFileOpt} {bootstrapFileOpt}
{logLevelOpt} {logLevelOpt}
{depositContractOpt}
--state-snapshot="{testnetDir/genesisFile}" """ & depositContractOpt, "\n", " ") --state-snapshot="{testnetDir/genesisFile}" """ & depositContractOpt, "\n", " ")

View File

@ -21,6 +21,7 @@ import # Unit test
./test_kvstore, ./test_kvstore,
./test_mocking, ./test_mocking,
./test_kvstore_sqlite3, ./test_kvstore_sqlite3,
./test_mainchain_monitor,
./test_ssz, ./test_ssz,
./test_state_transition, ./test_state_transition,
./test_sync_protocol, ./test_sync_protocol,

View File

@ -34,9 +34,9 @@ cd "$GIT_ROOT"
DATA_DIR="${SIMULATION_DIR}/node-$NODE_ID" DATA_DIR="${SIMULATION_DIR}/node-$NODE_ID"
PORT=$(( BASE_P2P_PORT + NODE_ID )) PORT=$(( BASE_P2P_PORT + NODE_ID ))
NAT_FLAG="--nat:extip:127.0.0.1" NAT_ARG="--nat:extip:127.0.0.1"
if [ "${NAT:-}" == "1" ]; then if [ "${NAT:-}" == "1" ]; then
NAT_FLAG="--nat:any" NAT_ARG="--nat:any"
fi fi
mkdir -p "$DATA_DIR/validators" mkdir -p "$DATA_DIR/validators"
@ -54,6 +54,11 @@ fi
rm -rf "$DATA_DIR/dump" rm -rf "$DATA_DIR/dump"
mkdir -p "$DATA_DIR/dump" mkdir -p "$DATA_DIR/dump"
SNAPSHOT_ARG=""
if [ -f "${SNAPSHOT_FILE}" ]; then
SNAPSHOT_ARG="--state-snapshot=${SNAPSHOT_FILE}"
fi
# if you want tracing messages, add "--log-level=TRACE" below # if you want tracing messages, add "--log-level=TRACE" below
cd "$DATA_DIR" && $BEACON_NODE_BIN \ cd "$DATA_DIR" && $BEACON_NODE_BIN \
--log-level=${LOG_LEVEL:-DEBUG} \ --log-level=${LOG_LEVEL:-DEBUG} \
@ -62,9 +67,9 @@ cd "$DATA_DIR" && $BEACON_NODE_BIN \
--node-name=$NODE_ID \ --node-name=$NODE_ID \
--tcp-port=$PORT \ --tcp-port=$PORT \
--udp-port=$PORT \ --udp-port=$PORT \
$NAT_FLAG \ $SNAPSHOT_ARG \
--state-snapshot=$SNAPSHOT_FILE \ $NAT_ARG \
$DEPOSIT_WEB3_URL_ARG \ $WEB3_ARG \
--deposit-contract=$DEPOSIT_CONTRACT_ADDRESS \ --deposit-contract=$DEPOSIT_CONTRACT_ADDRESS \
--rpc \ --rpc \
--rpc-address="127.0.0.1" \ --rpc-address="127.0.0.1" \

View File

@ -32,6 +32,86 @@ else
EXE_SUFFIX="" EXE_SUFFIX=""
fi fi
# to allow overriding the program names
MULTITAIL="${MULTITAIL:-multitail}"
TMUX="${TMUX:-tmux}"
GANACHE="${GANACHE:-ganache-cli}"
PROMETHEUS="${PROMETHEUS:-prometheus}"
TMUX_SESSION_NAME="${TMUX_SESSION_NAME:-nbc-sim}"
WAIT_GENESIS="${WAIT_GENESIS:-no}"
# Using tmux or multitail is an opt-in
USE_MULTITAIL="${USE_MULTITAIL:-no}"
type "$MULTITAIL" &>/dev/null || { echo "${MULTITAIL}" is missing; USE_MULTITAIL="no"; }
USE_TMUX="${USE_TMUX:-no}"
type "$TMUX" &>/dev/null || { echo "${TMUX}" is missing; USE_TMUX="no"; }
USE_GANACHE="${USE_GANACHE:-no}"
type "$GANACHE" &>/dev/null || { echo $GANACHE is missing; USE_GANACHE="no"; }
USE_PROMETHEUS="${LAUNCH_PROMETHEUS:-no}"
type "$PROMETHEUS" &>/dev/null || { echo $PROMETHEUS is missing; USE_PROMETHEUS="no"; }
# Prometheus config (continued inside the loop)
mkdir -p "${METRICS_DIR}"
cat > "${METRICS_DIR}/prometheus.yml" <<EOF
global:
scrape_interval: 1s
scrape_configs:
- job_name: "nimbus"
static_configs:
EOF
for i in $(seq $MASTER_NODE -1 $TOTAL_USER_NODES); do
# Prometheus config
cat >> "${METRICS_DIR}/prometheus.yml" <<EOF
- targets: ['127.0.0.1:$(( BASE_METRICS_PORT + i ))']
labels:
node: '$i'
EOF
done
COMMANDS=()
if [[ "$USE_TMUX" != "no" ]]; then
$TMUX new-session -s "${TMUX_SESSION_NAME}" -d
# maybe these should be moved to a user config file
$TMUX set-option -t "${TMUX_SESSION_NAME}" history-limit 999999
$TMUX set-option -t "${TMUX_SESSION_NAME}" remain-on-exit on
$TMUX set -t "${TMUX_SESSION_NAME}" mouse on
# We create a new window, so the above settings can take place
$TMUX new-window -d -t "${TMUX_SESSION_NAME}" -n "sim"
trap 'tmux kill-session -t "${TMUX_SESSION_NAME}"' SIGINT EXIT
fi
if [[ "$USE_GANACHE" != "no" ]]; then
if [[ "$USE_TMUX" != "no" ]]; then
$TMUX new-window -d -t $TMUX_SESSION_NAME -n "$GANACHE" "$GANACHE"
elif [[ "$USE_MULTITAIL" != "no" ]]; then
COMMANDS+=( " -cT ansi -t '$GANACHE'" )
else
$GANACHE &
fi
fi
if [[ "$USE_PROMETHEUS" != "no" ]]; then
if [[ "$USE_TMUX" != "no" ]]; then
$TMUX new-window -d -t $TMUX_SESSION_NAME -n "$PROMETHEUS" "cd '$METRICS_DIR' && $PROMETHEUS"
else
echo "$PROMETHEUS can be used currently only with USE_TMUX=1"
fi
fi
if [[ "$USE_TMUX" != "no" ]]; then
$TMUX select-window -t "${TMUX_SESSION_NAME}:sim"
fi
build_beacon_node () { build_beacon_node () {
OUTPUT_BIN=$1; shift OUTPUT_BIN=$1; shift
PARAMS="$CUSTOM_NIMFLAGS $DEFS $@" PARAMS="$CUSTOM_NIMFLAGS $DEFS $@"
@ -45,29 +125,48 @@ if [ ! -f "${LAST_VALIDATOR}" ]; then
echo Building "${DEPLOY_DEPOSIT_CONTRACT_BIN}" echo Building "${DEPLOY_DEPOSIT_CONTRACT_BIN}"
$MAKE NIMFLAGS="-o:\"$DEPLOY_DEPOSIT_CONTRACT_BIN\" $CUSTOM_NIMFLAGS $DEFS" deposit_contract $MAKE NIMFLAGS="-o:\"$DEPLOY_DEPOSIT_CONTRACT_BIN\" $CUSTOM_NIMFLAGS $DEFS" deposit_contract
if [ "$DEPOSIT_WEB3_URL_ARG" != "" ]; then if [ "$WEB3_ARG" != "" ]; then
DEPOSIT_CONTRACT_ADDRESS=$($DEPLOY_DEPOSIT_CONTRACT_BIN deploy $DEPOSIT_WEB3_URL_ARG) echo Deploying the validator deposit contract...
DEPOSIT_CONTRACT_ADDRESS=$($DEPLOY_DEPOSIT_CONTRACT_BIN deploy $WEB3_ARG)
echo Contract deployed at $DEPOSIT_CONTRACT_ADDRESS
export DEPOSIT_CONTRACT_ADDRESS export DEPOSIT_CONTRACT_ADDRESS
fi fi
DELAY_ARGS=""
# Uncomment this line to slow down the initial deposits.
# This will spread them across multiple blocks which is
# a more realistic scenario.
DELAY_ARGS="--min-delay=1 --max-delay=5"
MAKE_DEPOSITS_WEB3_ARG=$WEB3_ARG
if [[ "$WAIT_GENESIS" == "no" ]]; then
MAKE_DEPOSITS_WEB3_ARG=""
fi
$BEACON_NODE_BIN makeDeposits \ $BEACON_NODE_BIN makeDeposits \
--quickstart-deposits="${NUM_VALIDATORS}" \ --quickstart-deposits="${NUM_VALIDATORS}" \
--deposits-dir="$VALIDATORS_DIR" \ --deposits-dir="$VALIDATORS_DIR" \
$DEPOSIT_WEB3_URL_ARG \ $MAKE_DEPOSITS_WEB3_ARG $DELAY_ARGS \
--deposit-contract="${DEPOSIT_CONTRACT_ADDRESS}" --deposit-contract="${DEPOSIT_CONTRACT_ADDRESS}"
echo "All deposits prepared"
fi fi
if [ ! -f "${SNAPSHOT_FILE}" ]; then if [ ! -f "${SNAPSHOT_FILE}" ]; then
$BEACON_NODE_BIN \ if [[ "${WAIT_GENESIS}" == "no" ]]; then
--data-dir="${SIMULATION_DIR}/node-$MASTER_NODE" \ echo Creating testnet genesis...
createTestnet \ $BEACON_NODE_BIN \
--validators-dir="${VALIDATORS_DIR}" \ --data-dir="${SIMULATION_DIR}/node-$MASTER_NODE" \
--total-validators="${NUM_VALIDATORS}" \ createTestnet \
--output-genesis="${SNAPSHOT_FILE}" \ --validators-dir="${VALIDATORS_DIR}" \
--output-bootstrap-file="${NETWORK_BOOTSTRAP_FILE}" \ --total-validators="${NUM_VALIDATORS}" \
--bootstrap-address=127.0.0.1 \ --output-genesis="${SNAPSHOT_FILE}" \
--bootstrap-port=$(( BASE_P2P_PORT + MASTER_NODE )) \ --output-bootstrap-file="${NETWORK_BOOTSTRAP_FILE}" \
--genesis-offset=5 # Delay in seconds --bootstrap-address=127.0.0.1 \
--bootstrap-port=$(( BASE_P2P_PORT + MASTER_NODE )) \
--genesis-offset=5 # Delay in seconds
fi
fi fi
rm -f beacon_node.log rm -f beacon_node.log
@ -77,29 +176,6 @@ if [ -f "${MASTER_NODE_ADDRESS_FILE}" ]; then
rm "${MASTER_NODE_ADDRESS_FILE}" rm "${MASTER_NODE_ADDRESS_FILE}"
fi fi
# to allow overriding the program names
MULTITAIL="${MULTITAIL:-multitail}"
TMUX="${TMUX:-tmux}"
TMUX_SESSION_NAME="${TMUX_SESSION_NAME:-nbc-network-sim}"
# Using tmux or multitail is an opt-in
USE_MULTITAIL="${USE_MULTITAIL:-no}"
type "$MULTITAIL" &>/dev/null || { echo "${MULTITAIL}" is missing; USE_MULTITAIL="no"; }
USE_TMUX="${USE_TMUX:-no}"
type "$TMUX" &>/dev/null || { echo "${TMUX}" is missing; USE_TMUX="no"; }
# Prometheus config (continued inside the loop)
mkdir -p "${METRICS_DIR}"
cat > "${METRICS_DIR}/prometheus.yml" <<EOF
global:
scrape_interval: 1s
scrape_configs:
- job_name: "nimbus"
static_configs:
EOF
PROCESS_DASHBOARD_BIN="build/process_dashboard${EXE_SUFFIX}" PROCESS_DASHBOARD_BIN="build/process_dashboard${EXE_SUFFIX}"
if [[ ! -f "$PROCESS_DASHBOARD_BIN" ]]; then if [[ ! -f "$PROCESS_DASHBOARD_BIN" ]]; then
@ -107,6 +183,7 @@ if [[ ! -f "$PROCESS_DASHBOARD_BIN" ]]; then
fi fi
# use the exported Grafana dashboard for a single node to create one for all nodes # use the exported Grafana dashboard for a single node to create one for all nodes
echo Creating grafana dashboards...
"${PROCESS_DASHBOARD_BIN}" \ "${PROCESS_DASHBOARD_BIN}" \
--nodes=${TOTAL_NODES} \ --nodes=${TOTAL_NODES} \
--in="${SIM_ROOT}/beacon-chain-sim-node0-Grafana-dashboard.json" \ --in="${SIM_ROOT}/beacon-chain-sim-node0-Grafana-dashboard.json" \
@ -115,26 +192,20 @@ fi
# Kill child processes on Ctrl-C by sending SIGTERM to the whole process group, # Kill child processes on Ctrl-C by sending SIGTERM to the whole process group,
# passing the negative PID of this shell instance to the "kill" command. # passing the negative PID of this shell instance to the "kill" command.
# Trap and ignore SIGTERM, so we don't kill this process along with its children. # Trap and ignore SIGTERM, so we don't kill this process along with its children.
if [ "$USE_MULTITAIL" = "no" ]; then if [[ "$USE_MULTITAIL" == "no" && "$USE_TMUX" == "no" ]]; then
trap '' SIGTERM
trap 'pkill -P $$ beacon_node' SIGINT EXIT trap 'pkill -P $$ beacon_node' SIGINT EXIT
fi fi
COMMANDS=() LAST_WAITING_NODE=0
if [[ "$USE_TMUX" != "no" ]]; then
$TMUX new-session -s "${TMUX_SESSION_NAME}" -d
# maybe these should be moved to a user config file
$TMUX set-option -t "${TMUX_SESSION_NAME}" history-limit 999999
$TMUX set-option -t "${TMUX_SESSION_NAME}" remain-on-exit on
$TMUX set -t "${TMUX_SESSION_NAME}" mouse on
fi
for i in $(seq $MASTER_NODE -1 $TOTAL_USER_NODES); do for i in $(seq $MASTER_NODE -1 $TOTAL_USER_NODES); do
if [[ "$i" != "$MASTER_NODE" && "$USE_MULTITAIL" == "no" ]]; then if [[ "$i" != "$MASTER_NODE" && "$USE_MULTITAIL" == "no" ]]; then
# Wait for the master node to write out its address file # Wait for the master node to write out its address file
while [ ! -f "${MASTER_NODE_ADDRESS_FILE}" ]; do while [ ! -f "${MASTER_NODE_ADDRESS_FILE}" ]; do
if (( LAST_WAITING_NODE != i )); then
echo Waiting for $MASTER_NODE_ADDRESS_FILE to appear...
LAST_WAITING_NODE=i
fi
sleep 0.1 sleep 0.1
done done
fi fi
@ -142,6 +213,8 @@ for i in $(seq $MASTER_NODE -1 $TOTAL_USER_NODES); do
CMD="${SIM_ROOT}/run_node.sh ${i} --verify-finalization" CMD="${SIM_ROOT}/run_node.sh ${i} --verify-finalization"
if [[ "$USE_TMUX" != "no" ]]; then if [[ "$USE_TMUX" != "no" ]]; then
echo "Starting node $i..."
echo $TMUX split-window -t "${TMUX_SESSION_NAME}" "$CMD"
$TMUX split-window -t "${TMUX_SESSION_NAME}" "$CMD" $TMUX split-window -t "${TMUX_SESSION_NAME}" "$CMD"
$TMUX select-layout -t "${TMUX_SESSION_NAME}" tiled $TMUX select-layout -t "${TMUX_SESSION_NAME}" tiled
elif [[ "$USE_MULTITAIL" != "no" ]]; then elif [[ "$USE_MULTITAIL" != "no" ]]; then
@ -155,16 +228,13 @@ for i in $(seq $MASTER_NODE -1 $TOTAL_USER_NODES); do
else else
eval "${CMD}" & eval "${CMD}" &
fi fi
# Prometheus config
cat >> "${METRICS_DIR}/prometheus.yml" <<EOF
- targets: ['127.0.0.1:$(( BASE_METRICS_PORT + i ))']
labels:
node: '$i'
EOF
done done
if [[ "$USE_TMUX" != "no" ]]; then if [[ "$USE_TMUX" != "no" ]]; then
# kill the console window in the pane where the simulation is running
$TMUX kill-pane -t $TMUX_SESSION_NAME:sim.0
# kill the original console window
# (this one doesn't have the right history-limit)
$TMUX kill-pane -t $TMUX_SESSION_NAME:0.0 $TMUX kill-pane -t $TMUX_SESSION_NAME:0.0
$TMUX select-layout -t "${TMUX_SESSION_NAME}" tiled $TMUX select-layout -t "${TMUX_SESSION_NAME}" tiled
$TMUX attach-session -t "${TMUX_SESSION_NAME}" -d $TMUX attach-session -t "${TMUX_SESSION_NAME}" -d

View File

@ -37,8 +37,11 @@ MASTER_NODE_ADDRESS_FILE="${SIMULATION_DIR}/node-${MASTER_NODE}/beacon_node.addr
BASE_P2P_PORT=30000 BASE_P2P_PORT=30000
BASE_RPC_PORT=7000 BASE_RPC_PORT=7000
BASE_METRICS_PORT=8008 BASE_METRICS_PORT=8008
# Set DEPOSIT_WEB3_URL_ARG to empty to get genesis state from file, not using web3
# DEPOSIT_WEB3_URL_ARG=--web3-url=ws://localhost:8545 if [[ "$USE_GANACHE" == "yes" ]]; then
DEPOSIT_WEB3_URL_ARG="" WEB3_ARG=--web3-url=ws://localhost:8545
DEPOSIT_CONTRACT_ADDRESS="0x" else
WEB3_ARG=""
DEPOSIT_CONTRACT_ADDRESS="0x"
fi

View File

@ -0,0 +1,52 @@
import
unittest,
chronos, web3/ethtypes,
../beacon_chain/mainchain_monitor
type
MockDataProvider = ref object of DataProvider
method getBlockByHash*(p: MockDataProvider, hash: BlockHash): Future[BlockObject] {.
async
gcsafe
# raises: [Defect]
.} =
return BlockObject()
method onDisconnect*(p: MockDataProvider, handler: DisconnectHandler) {.
async
gcsafe
# raises: []
.} =
discard
method onDepositEvent*(p: MockDataProvider,
startBlock: Eth1BlockNumber,
handler: DepositEventHandler): Future[void] {.
async
gcsafe
# raises: []
.} =
discard
method close*(p: MockDataProvider): Future[void] {.
async
gcsafe
# raises: [Defect]
.} =
discard
method fetchDepositData*(p: MockDataProvider,
web3Block: BlockObject): Future[Eth1Block] {.
async
gcsafe
# raises: [Defect, CatchableError]
.} =
return Eth1Block()
suite "Eth1 Chain":
discard
suite "Mainchain monitor":
discard

1
vendor/nim-rocksdb vendored Submodule

@ -0,0 +1 @@
Subproject commit 08fec021c0f28f63d1221d40a655078b5b923d1b

2
vendor/nim-web3 vendored

@ -1 +1 @@
Subproject commit 969adf2f1ef42753ba26d5ab7eca01617c846792 Subproject commit 0ca608996289a2b2a4ea9bba715bb9f3e99de869