From 996887376d00ae4ba3ee9f84e9349ddea6ade365 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Wed, 23 Sep 2020 22:59:43 +0000 Subject: [PATCH 01/32] Update key derivation to latest EIP-2333 (#1633) ## Issue Addressed #1624 ## Proposed Changes Updates to match [EIP-2333](`https://eips.ethereum.org/EIPS/eip-2333`) ## Additional Info In order to have compatibility with the eth2.0-deposit-cli, [this PR](https://github.com/ethereum/eth2.0-deposit-cli/pull/108) must also be merged --- crypto/eth2_key_derivation/src/derived_key.rs | 1080 +++++++++-------- .../tests/eip2333_vectors.rs | 16 +- 2 files changed, 559 insertions(+), 537 deletions(-) diff --git a/crypto/eth2_key_derivation/src/derived_key.rs b/crypto/eth2_key_derivation/src/derived_key.rs index 74dfcfbf3..8ed6c9bd4 100644 --- a/crypto/eth2_key_derivation/src/derived_key.rs +++ b/crypto/eth2_key_derivation/src/derived_key.rs @@ -2,6 +2,7 @@ use crate::{lamport_secret_key::LamportSecretKey, secret_bytes::SecretBytes, Zer use num_bigint_dig::BigUint; use ring::hkdf::{KeyType, Prk, Salt, HKDF_SHA256}; use sha2::{Digest, Sha256}; +use std::convert::TryFrom; use zeroize::Zeroize; /// The byte size of a SHA256 hash. @@ -21,7 +22,7 @@ pub const R: &str = "52435875175126190479447740508185965837690552500527637822603 /// /// In EIP-2333 this value is defined as: /// -/// `ceil((1.5 * ceil(log2(r))) / 8)` +/// `ceil((3 * ceil(log2(r))) / 16)` pub const MOD_R_L: usize = 48; /// A BLS secret key that is derived from some `seed`, or generated as a child from some other @@ -81,9 +82,30 @@ fn derive_child_sk(parent_sk: &[u8], index: u32) -> ZeroizeHash { /// /// Equivalent to `HKDF_mod_r` in EIP-2333. fn hkdf_mod_r(ikm: &[u8]) -> ZeroizeHash { - let prk = hkdf_extract(b"BLS-SIG-KEYGEN-SALT-", ikm); - let okm = &hkdf_expand(prk, MOD_R_L); - mod_r(okm.as_bytes()) + // ikm = ikm + I2OSP(0,1) + let mut ikm_with_postfix = SecretBytes::zero(ikm.len() + 1); + ikm_with_postfix.as_mut_bytes()[..ikm.len()].copy_from_slice(ikm); + + // info = "" + I2OSP(L, 2) + let info = u16::try_from(MOD_R_L) + .expect("MOD_R_L too large") + .to_be_bytes(); + + let mut output = ZeroizeHash::zero(); + let zero_hash = ZeroizeHash::zero(); + + let mut salt = b"BLS-SIG-KEYGEN-SALT-".to_vec(); + while output.as_bytes() == zero_hash.as_bytes() { + let mut hasher = Sha256::new(); + hasher.update(salt.as_slice()); + salt = hasher.finalize().to_vec(); + + let prk = hkdf_extract(&salt, ikm_with_postfix.as_bytes()); + let okm = &hkdf_expand(prk, &info, MOD_R_L); + + output = mod_r(okm.as_bytes()); + } + output } /// Interprets `bytes` as a big-endian integer and returns that integer modulo the order of the @@ -145,7 +167,7 @@ fn parent_sk_to_lamport_pk(ikm: &[u8], index: u32) -> ZeroizeHash { /// Equivalent to `IKM_to_lamport_SK` in EIP-2333. fn ikm_to_lamport_sk(salt: &[u8], ikm: &[u8]) -> LamportSecretKey { let prk = hkdf_extract(salt, ikm); - let okm = hkdf_expand(prk, HASH_SIZE * LAMPORT_ARRAY_SIZE as usize); + let okm = hkdf_expand(prk, &[], HASH_SIZE * LAMPORT_ARRAY_SIZE as usize); LamportSecretKey::from_bytes(okm.as_bytes()) } @@ -159,7 +181,7 @@ fn hkdf_extract(salt: &[u8], ikm: &[u8]) -> Prk { /// Peforms a `HKDF-Expand` on the `pkr` (pseudo-random key), returning `l` bytes. /// /// Defined in [RFC5869](https://tools.ietf.org/html/rfc5869). -fn hkdf_expand(prk: Prk, l: usize) -> SecretBytes { +fn hkdf_expand(prk: Prk, info: &[u8], l: usize) -> SecretBytes { struct ExpandLen(usize); impl KeyType for ExpandLen { @@ -169,7 +191,7 @@ fn hkdf_expand(prk: Prk, l: usize) -> SecretBytes { } let mut okm = SecretBytes::zero(l); - prk.expand(&[], ExpandLen(l)) + prk.expand(&[info], ExpandLen(l)) .expect("expand len is constant and cannot be too large") .fill(okm.as_mut_bytes()) .expect("fill len is constant and cannot be too large"); @@ -307,528 +329,528 @@ mod test { /// Returns the copy-paste values from the spec. fn get_raw_vector() -> RawTestVector { RawTestVector { - seed: "0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", - master_sk: - "12513733877922233913083619867448865075222526338446857121953625441395088009793", - child_index: 0, - lamport_0: vec![ - "0x7b4a587eac94d7f56843e718a04965d4832ef826419b4001a3ad0ba77eb44a3b", - "0x90f45a712112122429412921ece5c30eb2a6daf739dc9034fc79424daeb5eff6", - "0xd061c2799de00b2be90eb1cc295f4c31e22d4b45c59a9b9b2554379bea7783cb", - "0x3ad17e4cda2913b5180557fbe7db04b5ba440ce8bb035ae27878d66fbfa50d2c", - "0xf5b954490933ad47f8bf612d4a4f329b3aa8914b1b83d59e15e271e2a087e002", - "0x95d68d505bf4ff3e5149bc5499cf4b2f00686c674a29a8d903f70e569557d867", - "0x1b59c76d9bb2170b220a87833582ede5970d4a336d91c99a812825afe963e056", - "0x4310ff73cfbbf7b81c39ecbf1412da33e9388c1a95d71a75e51fe12256551ceb", - "0xee696343f823e5716e16747f3bbae2fc6de233fe10eea8e45b4579018da0874f", - "0xae12a437aaa7ae59f7d8328944b6a2b973a43565c55d5807dc2faf223a33aa73", - "0x2a3ae0b47f145bab629452661ff7741f111272e33ec571030d0eb222e1ed1390", - "0x1a3ea396e8cbd1d97733ef4753d6840b42c0795d2d693f18e6f0e7b3fff2beb2", - "0x472429d0643c888bfdfe6e6ccfdeee6d345d60c6710859ac29fc289fd3656347", - "0xa32d4d955949b8bed0eb20f586d8fd516d6ddec84fbbc36998d692633c349822", - "0xe5ac8ac5ee1d40e53a7abf36e8269d5d5fce450a87feae8e59f432a44bcc7666", - "0xddf9e497ed78032fbd72d9b8abd5204d81c3475f29afa44cdf1ded8ea72dd1dc", - "0x945c62e88fb1e5f3c15ff57cd5eb1586ee93ec5ec80154c5a9c50241c5adae0a", - "0xc8868b50fc8423c96b7efa1ede4d3203a6b835dbeb6b2ababc58397e6b31d9dd", - "0x66de9bd86b50e2b6a755310520af655759c1753bff34b79a5cd63d6811fc8c65", - "0x5b13786c6068df7735343e5591393bea8aee92ac5826d6132bf4f5ebf1098776", - "0xa2038fc7d8e3cb2eda2bd303cfa76a9e5d8b88293918bec8b2fc03be75684f14", - "0x47a13f6b2308a50eded830fdee7c504bf49d1fe6a95e337b0825d0d77a520129", - "0xb534cdddcf1aa1c6b4cbba46d1db31b766d958e0a0306450bc031d1e3ed79d97", - "0x54aa051b754c31658377f7bff00b7deaa861e74cb12e1eb84216666e19b23d69", - "0x0220d57f63435948818eb376367b113c188e37451c216380f65d1ad55f73f527", - "0xf9dd2e391565534a4db84980433bf5a56250f45fe294fce2679bcf115522c081", - "0x1166591ee2ca59b9f4e525900f085141be8879c66ef18529968babeb87c44814", - "0xf4fa2e8de39bdbeb29b64d8b440d3a6c9a6ca5bdce543877eaee93c11bd70ab8", - "0x07f466d73b93db283b3f7bfaf9c39ae296adc376ab307ef12312631d0926790e", - "0xb2ecff93acb4fa44c1dbf8464b81734a863b6d7142b02f5c008907ea4dc9aaa1", - "0xa1d9c342f6c293ac6ef8b5013cba82c4bad6ed7024d782948cb23cd490039ba1", - "0xc7d04a639ba00517ece4dbc5ef4aaf20e0ccde6e4a24c28936fabe93dec594db", - "0xe3cbb9810472d9dd1cdb5eed2f74b67ea60e973d2d2e897bd64728c9b1aa0679", - "0xe36884703413958ff2aba7a1f138a26d0ac0a371270f0169219beb00a5add5f0", - "0xe5ea300a09895b3f98de5232d92a36d5611cbcf9aaf9e7bb20cf6d1696ad1cb4", - "0xc136cda884e18175ab45148ed4f9d0d1a3c5e11ad0275058e61ae48eb151a81f", - "0x3ee1101e944c040021187e93b6e0beb1048c75fb74f3fdd67756b1c8517a311f", - "0x016964fd6fc32b9ad07a630949596715dee84d78230640368ff0929a280cf3a2", - "0xe33865fc03120b94333bb754fd097dc0f90e69ff6fd221d6aae59fcf2d762d76", - "0xe80bb3515a09ac6ecb4ec59de22701cdf954b1ae8a677fd85508c5b041f28058", - "0x3889af7cd325141ec288021ede136652a0411d20364005b9d3ca9102cb368f57", - "0x18dad0bc975cf8800addd54c7867389d3f7fe1b97d348bd8412a6cbfb75c520a", - "0x09035218686061ee91bd2ad57dc6fb6da7243b8177a153484524b2b228da5314", - "0x688fd7a97551c64eae33f91abb073a46eafbbacd5595c6bac2e57dd536acdfe2", - "0x1fc164dce565a1d0da59cc8048b334cc5eb84bf04de2399ddb847c22a7e32ab7", - "0xa2a340ba05c8a30dd1cab886a926b761758eba0e41b5c4c5dfd4a42f249655c1", - "0xc43dffe01479db836a6a1a74564b297fad0d69c6b06cf593f6db9f26b4f307d5", - "0x73cef7f3ff724a30a79e1dca74cef74954afeefa2e476c4dec65afe50c16c5c4", - "0xa54002253ab7b95cc5b664b3f08976400475cc56f170b939f6792e730ff5170b", - "0x9ade43053d41afebc002f09476dffd1b13ecbf67f810791540b92ca56d5e63e4", - "0x234e7cbfbe45b22a871db26738fa05de09213a925439d7f3e5108132e521b280", - "0x066b712417332c7cfca871fb1bb5839f0341acf9266229603a3eddbc8a93b59f", - "0xb5857acdcf636330da2cfcc99c81d9fdbd20c506a3c0e4f4f6a139d2a64f051c", - "0xe119908a150a49704b6bbba2c470cd619a0ae10dd9736e8d491890e3c8509fff", - "0xb8a5c5dbb51e6cb73cca95b4ad63ea3c7399cd16b05ab6261535495b3af2ca51", - "0x05624a1d4d2d2a31160bc48a6314bbf13eaddf56cddb0f0aa4ed3fb87f8b479f", - "0x483daceff1c3baa0ed0f3be7e534eebf5f4aed424ecd804edfbf5c56b3476b50", - "0x424d04694e7ae673707c77eb1c6d0996d250cfab6832ee3506a12e0384a3c5c9", - "0xa11fed0ed8057966bfe7136a15a814d06a516fbc9d44aeef87c509137a26190e", - "0x3694d22d1bc64658f3adbe2cc9f1716aee889066e0950e0b7a2fd576ed36bb76", - "0x49a13000a87f39f93d0ae9c3a4cfccbf440c0a75cce4c9d70dac627b6d6958b3", - "0xb3ff0cdd878d5ac1cb12e7d0b300d649fdd008800d498ae4f9fbf9510c74249a", - "0xe52a867cfb87d2fe7102d23d8d64925f7b75ca3f7d6bb763f7337352c255e0be", - "0x6513b372e4e557cca59979e48ec27620e9d7cdb238fcf4a9f19c3ba502963be0", - "0x9f69d82d4d51736902a987c8b5c30c2b25a895f2af5d2c846667ff6768bcc774", - "0x049a220dbe3340749f94643a429cb3cba3c92b561dc756a733d652d838728ab3", - "0x4fa2cd877aa115b476082b11053309f3537fa03d9158085f5f3f4bab6083e6da", - "0xed12db4069eb9f347735816afcee3fe43d4a6999fef8240b91bf4b05447d734f", - "0x3ecbe5eda469278f68548c450836a05cc500864664c7dda9b7526f084a891032", - "0x690d8f928fc61949c22e18cceaa2a446f8e1b65bd2e7af9e0a8e8284134ab3d2", - "0x99e09167a09f8261e7e8571d19148b7d7a75990d0702d9d582a2e4a96ac34f8e", - "0x6d33931693ed7c2e1d080b6a37da52c279a06cec5f534305819f7adf7db0afe3", - "0xc4b735462a9a656e28a52b1d4992ea9dea826b858971d698453a4be534d6bb70", - "0xedf92b10302dc41f8d362b360f4c2ef551d50e2ded012312c964002d2afc46d7", - "0x58f6691cca081ae5c3661dd171b87cc49c90359bb03cc0e57e503f7fcf14aefc", - "0x5d29b8b4ee295a73c4a8618927b3d14b76c7da049133a2257192b10be8c17a6a", - "0x646802fa42801e0ae24011fb4f62e87219ef1da01f7fc14bf8d6bd2d9e7c21f1", - "0x23abf45eee65cc4c1e95ccab42ad280a00bb3b14d243e2021a684075f900141e", - "0x2b1ae95c975bf9c387eae506fdb5e58afd2d198f00a21cd3fddb5855e8021e4d", - "0x0ef9f6e1c0583493d343e75f9c0c557fa6da0dc12b17a96c5757292916b72ee3", - "0x04c7fc76195c64a3285af14161077c045ff6ddbb67c0ff91b080f98eb6781e5c", - "0xba12679b97027d0e7076e6d19086c07792eaa7f78350842fbef8ddf5bcd3ecc0", - "0xcead458e6799df4d2f6cbf7f13cb3afec3441a354816e3071856ed49cbdbb1a7", - "0xbe6c56256556bb5c6727a1d9cb641d969677f56bb5ad7f8f7a7c9cfd128427b4", - "0xc80f11963ff40cb1888054b83c0463d32f737f2e7d42098e639023db0dfc84d4", - "0xac80006c1296bcfde86697efebb87fb0fddfb70dd34dd2ee4c152482af4687eb", - "0xbb7d13ce184249df4576fc3d13351e1683500e48726cd4198423f14f9094068b", - "0x1b2d9c40c55bd7362664fa46c1268e094d56c8193e3d991c08dc7a6e4ca14fa1", - "0x9bd236254d0565f5b2d24552d4b4d732de43b0adaa64ecd8be3efc6508577591", - "0x38078cefccc04e8312d79e0636e0e3157434c50a2ad4e3e87cc6584c41eec8b5", - "0xb5d15a8527ff3fa254ba61ffceb02d2570b53361894f351a9e839c0bb716857d", - "0x6763dad684bf2e914f40ae0a7ee0cdf12c97f41fc05a485d5991b4daad21a3f8", - "0xc80363c20df589333ecbe05bd5f2c19942ebc2593626dc50d00835c40fb8d005", - "0x48502b56ae93acd2794f847cbe825525d5d5f59f0f75c67aff84e5338776b3af", - "0xfd8e033493ba8af264a855a78ab07f37d936351d2879b95928909ed8df1b4f91", - "0x11f75bee9eac7356e65ebc7f004ccdc1da80807380d69143293d1421f50b1c97", - "0x903a88a3ebe84ca1c52a752b1faffa9ca1daedac9cbf1aa70942efc9beb44b79", - "0x2c0dcd68837f32a69da651045ad836b8cd6b48f2c8c5d73a3bd3bba6148d345a", - "0x0aa0f49b3476f3fdb6393f2ab601e0009586090b72ee54a525734f51598960d5", - "0xf7a789f013f702731656c562caa15b04cb7c9957376c4d80b8839167bb7fa626", - "0x4e0be1b19e305d82db3fd8affd67b0d2559da3edbfb08d19632a5cc46a90ed07", - "0x3caaccfc546d84d543eaf4f4c50c9c8fd831c12a8de56fdb9dfd04cc082882fe", - "0x894f6a01fd34f0642077e22981752011678548eb70eb55e8072c1caffc16fe02", - "0xae7eb54adaa68679348ea3537a49be669d1d61001fbab9fac259ba727dbc9a1a", - "0x291a1cbdceff957b5a65440ab67fb8672de881230fe3108a15ca487c2662c2c7", - "0x891d43b867137bf8beb9df4da2d951b5984a266a8cd74ec1593801d005f83f08", - "0xc558407f6491b37a10835e0ad7ce74f4e368aa49157a28873f7229310cb2d7fd", - "0x9ce061b0a072e1fe645f3479dac089b5bfb78cfa6cfbe5fd603bcdb504711315", - "0xa8e30d07b09275115dd96472ecf9bc316581caf307735176ca226d4cd9022925", - "0x918ee6d2efba7757266577691203f973cf4f4cac10f7d5f86acd2a797ff66583", - "0xfa31ba95e15d1635d087522f3d0da9cf7acac4ed6d0ac672654032a3c39244a6", - "0xf2952b58f015d6733af06938cd1f82fbddb3b796823bee7a3dbffa04efc117c2", - "0x46f8f742d3683de010ede528128d1181e8819f4252474f51371a177bfa518fa4", - "0x4ca1cc80094f2910cf83a9e65ad70e234690ffb9142793911ec7cf71663545b3", - "0x381965037b5725c71bfa6989d4c432f6611de8e8ec387f3cfc0dcb1a15191b73", - "0x2562b88ed3b86ba188be056805a3b7a47cb1a3f630d0e2f39647b0792ec6b7d8", - "0x565f6d14e7f22724f06d40f54465ad40d265b6de072b34a09d6e37a97a118cd8", - "0xc2982c861ad3278063b4a5f584eaf866db684cc4e712d64230fc9ee33bb4253b", - "0xfd806c91927e549d8d400ab7aa68dbe60af988fbabf228483ab0c8de7dab7eee", - "0xafae6ff16c168a3a3b5c2f1742d3f89fa4777c4bd0108f174014debf8f4d629c", - "0xaf5a4be694de5e53632be9f1a49bd582bf76002259460719197079c8c4be7e66", - "0xa8df4a4b4c5bf7a4498a11186f8bb7679137395f28e5c2179589e1c1f26504b5", - "0xce8b77c64c646bb6023f3efaed21ca2e928e21517422b124362cf8f4d9667405", - "0x62e67a8c423bc6c6c73e6cd8939c5c1b110f1a38b2ab75566988823762087693", - "0x7e778f29937daaa272d06c62d6bf3c9c0112d45a3df1689c602d828b5a315a9f", - "0xe9b5abd46c2377e602ff329050afa08afe152f4b0861db8a887be910ff1570bf", - "0xa267b1b2ccd5d96ae8a916b0316f06fafb886b3bb41286b20763a656e3ca0052", - "0xb8ed85a67a64b3453888a10dedf4705bd27719664deff0996a51bb82bc07194f", - "0x57907c3c88848f9e27bc21dd8e7b9d61de48765f64d0e943e7a6bb94cc2021ab", - "0xd2f6f1141a3b76bf9bf581d49091142944c7f9f323578f5bdd5522ba32291243", - "0xc89f104200ed4c5d5f7046d99e68ae6f8ec31e2eeceb568eb05087e3aa546a74", - "0xc9f367fae45c39299693b134229bb6dd0da112fd1a7d19b7f4772c01e5cbe479", - "0x64e2d4ad51948764dd578d26357e29e8e4d076d65c05cffdf8211b624fefe9ac", - "0xf9a9b4e6d5be7fc051df8ecd9c389d16b1af86c749308e6a23f7ff4871f0ba9a", - "0x0d2b2a228b86ebf9499e1bf7674335087ced2eb35ce0eb90954a0f75751a2bf4", - "0xff8531b45420a960d6e48ca75d77758c25733abde83cd4a6160beae978aa735e", - "0xd6d412bd1cb96a2b568d30e7986b7e8994ca92fd65756a758295499e11ea52b6", - "0xad8533fccbecdd4a0b00d648bfe992360d265f7be70c41d9631cefad5d4fe2f6", - "0x31fbf2afb8d5cc896d517cfc5201ee24527e8d283f9c37ca10233bef01000a20", - "0x2fd67b7365efc258131eb410f46bf3b1cbd3e9c76fd6e9c3e86c9ff1054116ff", - "0xab6aa29f33d18244be26b23abadb39679a8aa56dafc0dd7b87b672df5f5f5db6", - "0xbad3b0f401ca0a53a3d465de5cecd57769ec9d4df2c04b78f8c342a7ed35bbee", - "0xbdc24d46e471835d83ce8c5b9ecbe675aab2fd8f7831c548e8efd268c2ee2232", - "0x87265fabd7397d08f0729f13a2f3a25bbc8c874b6b50f65715c92b62f665f925", - "0xa379fd268e7ff392c067c2dd823996f72714bf3f936d5eeded71298859f834cb", - "0xf3ab452c9599ebfbb234f72a86f3062aed12ae1f634abbe542ff60f5cefc1fcf", - "0x2b17ebb053a3034c07da36ed2ba42c25ad8e61dec87b5527f5e1c755eb55405a", - "0x305b40321bd67bf48bfd121ee4d5d347268578bd4b8344560046594771a11129", - "0xe7029c9bea020770d77fe06ca53b521b180ad6a9e747545aadc1c74beef7241c", - "0xabc357cec0f4351a5ada22483d3b103890392f8d8f9cb8073a61969ed1be4e08", - "0x97f88c301946508428044d05584dc41af2e6a0de946de7d7f5269c05468afe20", - "0xbdc08fe8d6f9a05ad8350626b622ad8eec80c52331d154a3860c98676719cfbd", - "0x161590fc9f7fcf4eaba2f950cf588e6da79e921f139d3c2d7ebe017003a4799e", - "0x91b658db75bc3d1954bfde2ef4bc12980ff1688e09d0537f170c9ab47c162320", - "0x76d995f121406a63ce26502e7ec2b653c221cda357694a8d53897a99e6ce731e", - "0x3d6b2009586aceb7232c01259bb9428523c02b0f42c2100ec0d392418260c403", - "0x14ca74ecbc8ec0c67444c6cb661a2bce907aa2a1453b11f16002b815b94a1c49", - "0x553b4dc88554ebe7b0a3bd0813104fd1165a1f950ceace11f5841aa74b756d85", - "0x4025bf4ad86751a156d447ce3cabafde9b688efcdafd8aa4be69e670f8a06d9e", - "0x74260cf266997d19225e9a0351a9acfa17471fccdf5edc9ccc3bb0d23ef551c5", - "0xf9dbca3e16d234e448cf03877746baeb62a8a25c261eff42498b1813565c752a", - "0x2652ec98e05c1b6920fb6ddc3b57e366d514ffa4b35d068f73b5603c47f68f2f", - "0x83f090efeb36db91eb3d4dfbb17335c733fce7c64317d0d3324d7caaaf880af5", - "0x1e86257f1151fb7022ed9ed00fb961a9a9989e58791fb72043bb63ed0811791c", - "0xd59e4dcc97cba88a48c2a9a2b29f79125099a39f74f4fb418547de8389cd5d15", - "0x875a19b152fe1eb3fe1de288fa9a84864a84a79bac30b1dbd70587b519a9770e", - "0x9c9dc2d3c8f2f6814cfc61b42ee0852bbaf3f523e0409dd5df3081b750a5b301", - "0xf6f7f81c51581c2e5861a00b66c476862424151dd750efeb20b7663d552a2e94", - "0x723fcb7ca43a42483b31443d4be9b756b34927176f91a391c71d0b774c73a299", - "0x2b02d8acf63bc8f528706ed4d5463a58e9428d5b71d577fd5daa13ba48ac56cf", - "0x2ff6911f574c0f0498fc6199da129446b40fca35ccbf362bc76534ba71c7ca22", - "0x1ef4b959b11bc87b11e4a5f84b4d757c6bdcfad874acec9a6c9eee23dc4bbe1b", - "0x68e2df9f512be9f64b7e3a2dee462149dac50780073d78b569a20256aea5f751", - "0xd1a3682e12b90ae1eab27fc5dc2aef3b8e4dbb813925e9a91e58d6c9832767b6", - "0x75778ccc102d98c5e0b4b83f7d4ef7fe8bc7263cc3317723001cb0b314d1e9e8", - "0xc7f44e2cead108dc167f0036ac8a278d3549cc3dd5cc067d074ccad9b1d9f8d4", - "0x4cba0223c5df2796b0ee9fbc084d69f10e6aedda8f0cf86171bebb156ede676c", - "0x628deda825661f586a5713e43c806fdd55e1a53fbe90a4ddb5f3786570740954", - "0xfc82a253bc7e0ac96252b238fbb411a54e0adf78d089f804a7fc83a4959b401e", - "0x72a6491f5daae0ceb85b61a5ed69009dd2a167c64cb35cabf38b846e27268e9d", - "0xee139a913d4fcf25ba54bb36fc8051b91f2ec73ba820cc193c46fb2f7c37a106", - "0x7f75021f2b1d0c78859478e27f6f40646b5776c060f1a5f6f0944c840a0121f8", - "0x5b60a1b78feca1d2602ac8110d263ad6b3663cbf49e6bdc1077b4b80af2feb6f", - "0xd61f15d80b1e88469b6a76ed6a6a2b94143b6acc3bd717357264818f9f2d5c6d", - "0xea85da1780b3879a4d81b685ba40b91c060866abd5080b30fbbb41730724a7dd", - "0xb9b9da9461e83153f3ae0af59fbd61febfde39eb6ac72db5ed014797495d4c26", - "0xf737762fe8665df8475ff341b3762aaeb90e52974fe5612f5efd0fc1c409d7f8", - "0xaaa25d934a1d5aa6b2a1863704d7a7f04794ed210883582c1f798be5ca046cf7", - "0x932f46d0b6444145221b647f9d3801b6cb8b1450a1a531a959abdaacf2b5656b", - "0xf4a8b0e52f843ad27635c4f5a467fbf98ba06ba9a2b93a8a97170b5c41bf4958", - "0x196ed380785ee2925307ec904161dc02a4596a55499e5b0a3897f95485b3e74a", - "0x772e829a405219e4f8cd93a1ef15c250be85c828c1e29ef6b3f7b46958a85b44", - "0xd66cfc9af9941515d788f9f5e3b56fddb92464173ddb67b83bf265e7ea502170", - "0xf5b040bfc246425278e2423b1953d8ad518de911cf04d16c67d8580a09f90e62", - "0xd2d18b2ae8a53dde14b4000e5e7e414505825f50401a3797dd8820cf510dc448", - "0xc01dcc064e644266739cd0ec7edf92fc2ef8e92e0beedf0e8aa30efcff1644fe", - "0x24720d325913ba137daf031924ad3bfaa1c8c00a53a2d048fe5667aef45efce3", - "0x70a24e1c89b3ea78d76ef458d498dcb5b8561d484853b2a8b2adcd61869857df", - "0x0ff3313997f14e1b1dcd80f1d62c58aaefb19efd7c0ea15dde21aa4e2a516e80", - "0x960c1f50062a4df851638f42c0259b6e0a0217300884f13a3c5c8d94adb34f21", - "0xb71ca7cc8578149da556131268f4625b51620dfc3a6e9fbd47f5df03afbd410e", - "0xa1a3eeec0addec7b9e15f416a07608a1b5d94f0b42d5c203b8ced03a07484f5b", - "0xa4bb8b059aa122ca4652115b83b17af80cfbea0d3e1e8979a396a667f94e85f3", - "0x31c4d2f252167fe2a4d41944224a80b2f1afaf76f8dd6a3d52d71751849e44bb", - "0x79642dd6a255f96c9efe569304d58c327a441448db0431aa81fe072d0d359b52", - "0x42a4b504714aba1b67defe9458fff0c8cb1f216dcab28263cef67a65693b2036", - "0xe3d2f6a9d882d0f026ef316940dfcbf131342060ea28944475fe1f56392c9ad2", - "0x986af9aeff236394a0afa83823e643e76f7624e9bfd47d5468f9b83758a86caa", - "0xafe2de6ede50ee351d63ed38d1f2ae5203174c731f41bbed95db467461ad5492", - "0x9ad40f0785fe1c8a5e4c3342b3c91987cd47a862ece6573674b52fa0456f697a", - "0xde4cde6d0fc6def3a89b79da0e01accdbec049f1c9471d13a5d59286bd679af1", - "0xecd0d1f70116d6b3ae21c57fb06ad90eed33d040e2c5c3d12714b3be934fa5ce", - "0x3c53c5bf2d1b1d4038e1f0e8a2e6d12e0d4613d5cd12562578b6909921224c10", - "0x36087382b37e9e306642cc6e867e0fb2971b6b2b28b6caf2f9c96b790e8db70a", - "0xa957496d6a4218a19998f90282d05bd93e6baabf55e55e8a5f74a933a4dec045", - "0x077d6f094e8467a21f02c67753565ec5755156015d4e86f1f82a22f9cf21c869", - "0x12dd3b1f29e1462ca392c12388a77c58044151154cf86f23873f92a99b6bb762", - "0x7fdbcdedcc02ecf16657792bd8ef4fa4adeee497f30207d4cc060eb0d528b26b", - "0x245554b12bf8edf9e9732d6e2fa50958376e355cb695515c94676e64c6e97009", - "0xccd3b1841b517f7853e35f85471710777e437a8665e352a0b61c7d7083c3babc", - "0xd970545a326dcd92e31310d1fdce3703dff8ef7c0f3411dfa74fab8b4b0763ac", - "0xd24163068918e2783f9e79c8f2dcc1c5ebac7796ce63070c364837aac91ee239", - "0x256a330055357e20691e53ca5be846507c2f02cfde09cafb5809106f0af9180e", - "0xfa446a5d1876c2051811af2a341a35dbcd3f7f8e2e4f816f501139d27dd7cd82", - "0xbafbc7a8f871d95736a41e5721605d37e7532e41eb1426897e33a72ed2f0bf1d", - "0x8055af9a105b6cf17cfeb3f5320e7dab1a6480500ff03a16c437dfec0724c290", - "0x1de6ee3e989497c1cc7ca1d16b7b01b2f336524aa2f75a823eaa1716c3a1a294", - "0x12bb9508d646dda515745d104199f71276d188b3e164083ad27dfdcdc68e290b", - "0x7ea9f9939ad4f3b44fe7b780e0587da4417c34459b2996b3a449bb5b3ff8c8cb", - "0xa88d2f8f35bc669aa6480ce82571df65fea366834670b4084910c7bb6a735dde", - "0x9486e045adb387a550b3c7a603c30e07ed8625d322d1158f4c424d30befe4a65", - "0xb283a70ba539fe1945be096cb90edb993fac77e8bf53616bde35cdcaa04ab732", - "0xab39a81558e9309831a2caf03e9df22e8233e20b1769f16e613debcdb8e2610f", - "0x1fc12540473fbbad97c08770c41f517ce19dc7106aa2be2e9b77867046627509", - "0xec33dbec9d655c4c581e07d1c40a587cf3217bc8168a81521b2d0021bd0ec133", - "0xc8699e3b41846bc291209bbb9c06f565f66c6ccecbf03ebc27593e798c21fe94", - "0x240d7eae209c19d453b666c669190db22db06279386aa30710b6edb885f6df94", - "0xb181c07071a750fc7638dd67e868dddbeeee8e8e0dcbc862539ee2084674a89e", - "0xb8792555c891b3cbfddda308749122a105938a80909c2013637289e115429625", - "0xfe3e9e5b4a5271d19a569fee6faee31814e55f156ba843b6e8f8dc439d60e67a", - "0x912e9ba3b996717f89d58f1e64243d9cca133614394e6ae776e2936cf1a9a859", - "0xa0671c91a21fdfd50e877afa9fe3974aa3913855a2a478ae2c242bcdb71c73d7", - "0x5b55d171b346db9ba27b67105b2b4800ca5ba06931ed6bd1bafb89d31e6472e6", - "0x68438458f1af7bd0103ef33f8bc5853fa857b8c1f84b843882d8c328c595940d", - "0x21fe319fe8c08c1d00f977d33d4a6f18aecaa1fc7855b157b653d2d3cbd8357f", - "0x23cce560bc31f68e699ece60f21dd7951c53c292b3f5522b9683eb2b3c85fc53", - "0x917fa32d172c352e5a77ac079df84401cdd960110c93aa9df51046d1525a9b49", - "0x3fc397180b65585305b88fe500f2ec17bc4dccb2ec254dbb72ffb40979f14641", - "0xf35fb569e7a78a1443b673251ac70384abea7f92432953ca9c0f31c356be9bd9", - "0x7955afa3cd34deb909cd031415e1079f44b76f3d6b0aaf772088445aaff77d08", - "0x45c0ca029356bf6ecfc845065054c06024977786b6fbfaea74b773d9b26f0e6c", - "0xe5c1dac2a6181f7c46ab77f2e99a719504cb1f3e3c89d720428d019cb142c156", - "0x677b0e575afcccf9ddefc9470e96a6cfff155e626600b660247b7121b17b030a", - "0xbeed763e9a38277efe57b834a946d05964844b1f51dba2c92a5f3b8d0b7c67d0", - "0x962b17ed1a9343d8ebfae3873162eef13734985f528ca06c90b0c1e68adfdd89", - ], - lamport_1: vec![ - "0xb3a3a79f061862f46825c00fec4005fb8c8c3462a1eb0416d0ebe9028436d3a9", - "0x6692676ce3b07f4c5ad4c67dc2cf1dfa784043a0e95dd6965e59dc00b9eaff2d", - "0xbf7b849feb312db230e6e2383681b9e35c064e2d037cbc3c9cc9cd49220e80c9", - "0xa54e391dd3b717ea818f5954eec17b4a393a12830e28fabd62cbcecf509c17dc", - "0x8d26d800ac3d4453c211ef35e9e5bb23d3b9ede74f26c1c417d6549c3110314d", - "0xbb8153e24a52398d92480553236850974576876c7da561651bc551498f184d10", - "0x0d30e0e203dc4197f01f0c1aba409321fbf94ec7216e47ab89a66fb45e295eff", - "0x01dc81417e36e527776bf37a3f9d74a4cf01a7fb8e1f407f6bd525743865791d", - "0xa6318e8a57bec438245a6834f44eb9b7fb77def1554d137ea12320fc572f42c9", - "0xd25db9df4575b595130b6159a2e8040d3879c1d877743d960bf9aa88363fbf9f", - "0x61bb8baeb2b92a4f47bb2c8569a1c68df31b3469e634d5e74221bc7065f07a96", - "0xb18962aee4db140c237c24fec7fd073b400b2e56b0d503f8bc74a9114bf183bf", - "0x205473cc0cdab4c8d0c6aeceda9262c225b9db2b7033babfe48b7e919751a2c6", - "0xc5aa7df7552e5bb17a08497b82d8b119f93463ccb67282960aee306e0787f228", - "0x36da99e7d38ce6d7eab90ea109ba26615ad75233f65b3ae5056fba79c0c6682a", - "0xd68b71bba6266b68aec0df39b7c2311e54d46a3eab35f07a9fe60d70f52eec58", - "0xbbe56f1274ada484277add5cb8c90ef687d0b69a4c95da29e32730d90a2d059f", - "0x0982d1d1c15a560339d9151dae5c05e995647624261022bbedce5dce8a220a31", - "0x8ef54ad546d2c6144fc26e1e2ef92919c676d7a76cfdfb5c6a64f09a54e82e71", - "0x1e3ac0133eef9cdbeb590f14685ce86180d02b0eea3ef600fd515c38992b1f26", - "0x642e6b1c4bec3d4ba0ff2f15fbd69dcb57e4ba8785582e1bc2b452f0c139b590", - "0xca713c8cf4afa9c5d0c2db4fc684a8a233b3b01c219b577f0a053548bedf8201", - "0xd0569ba4e1f6c02c69018b9877d6a409659cb5e0aa086df107c2cc57aaba62da", - "0x4ebe68755e14b74973e7f0fa374b87cee9c370439318f5783c734f00bb13e4b5", - "0x788b5292dc5295ae4d0ea0be345034af97a61eec206fda885bbc0f049678c574", - "0x0ebd88acd4ae195d1d3982038ced5af1b6f32a07349cf7fffbff3ce410c10df2", - "0xc7faf0a49234d149036c151381d38427b74bae9bd1601fc71663e603bc15a690", - "0xc5247bf09ebe9fa4e1013240a1f88c703f25a1437196c71ee02ca3033a61f946", - "0x719f8c68113d9f9118b4281e1f42c16060def3e3eeef15f0a10620e886dc988f", - "0x28da4f8d9051a8b4d6158503402bdb6c49ba2fb1174344f97b569c8f640504e6", - "0x96f6773576af69f7888b40b0a15bc18cc9ec8ca5e1bb88a5de58795c6ddf678e", - "0x8d80d188a4e7b85607deccf654a58616b6607a0299dd8c3f1165c453fd33d2e4", - "0x9c08dcc4f914486d33aa24d10b89fd0aabcc635aa2f1715dfb1a18bf4e66692a", - "0x0ff7045b5f6584cc22c140f064dec0692762aa7b9dfa1defc7535e9a76a83e35", - "0x8e2dae66fa93857b39929b8fc531a230a7cfdd2c449f9f52675ab5b5176461d5", - "0xf449017c5d429f9a671d9cc6983aafd0c70dd39b26a142a1d7f0773de091ac41", - "0xed3d4cab2d44fec0d5125a97b3e365a77620db671ecdda1b3c429048e2ebdae6", - "0x836a332a84ee2f4f5bf24697df79ed4680b4f3a9d87c50665f46edaeed309144", - "0x7a79278754a4788e5c1cf3b9145edb55a2ba0428ac1c867912b5406bb7c4ce96", - "0x51e6e2ba81958328b38fd0f052208178cec82a9c9abd403311234e93aff7fa70", - "0x217ec3ec7021599e4f34410d2c14a8552fff0bc8f6894ebb52ec79bf6ec80dc9", - "0x8a95bf197d8e359edabab1a77f5a6d04851263352aa46830f287d4e0564f0be0", - "0x60d0cbfb87340b7c92831872b48997ce715da91c576296df215070c6c20046d4", - "0x1739fbca476c540d081b3f699a97387b68af5d14be52a0768d5185bc9b26961b", - "0xac277974f945a02d89a0f8275e02de9353e960e319879a4ef137676b537a7240", - "0x959b7640821904ba10efe8561e442fbdf137ccb030aee7472d10095223e320ba", - "0xdba61c8785a64cb332342ab0510126c92a7d61f6a8178c5860d018d3dad571c6", - "0xc191fb6a92eb1f1fb9e7eb2bdecd7ec3b2380dd79c3198b3620ea00968f2bd74", - "0x16ef4e88e182dfc03e17dc9efaa4a9fbf4ff8cb143304a4a7a9c75d306729832", - "0x39080e4124ca577ff2718dfbcb3415a4220c5a7a4108729e0d87bd05adda5970", - "0xa29a740eef233956baff06e5b11c90ed7500d7947bada6da1c6b5d9336fc37b6", - "0x7fda7050e6be2675251d35376bacc895813620d245397ab57812391d503716ee", - "0x401e0bf36af9992deb87efb6a64aaf0a4bc9f5ad7b9241456b3d5cd650418337", - "0x814e70c57410e62593ebc351fdeb91522fe011db310fcf07e54ac3f6fefe6be5", - "0x03c1e52ecbef0d79a4682af142f012dc6b037a51f972a284fc7973b1b2c66dcf", - "0x57b22fb091447c279f8d47bdcc6a801a946ce78339e8cd2665423dfcdd58c671", - "0x53aeb39ab6d7d4375dc4880985233cba6a1be144289e13cf0bd04c203257d51b", - "0x795e5d1af4becbca66c8f1a2e751dcc8e15d7055b6fc09d0e053fa026f16f48f", - "0x1cd02dcd183103796f7961add835a7ad0ba636842f412643967c58fe9545bee4", - "0x55fc1550be9abf92cacb630acf58bad11bf734114ebe502978a261cc38a4dd70", - "0x6a044e0ea5c361d3fb2ca1ba795301e7eb63db4e8a0314638f42e358ea9cfc3e", - "0x57d9f15d4db199cbcb7cbd6524c52a1b799d52b0277b5a270d2985fcee1e2acb", - "0x66c78c412e586bd01febc3e4d909cc278134e74d51d6f60e0a55b35df6fb5b09", - "0x1076799e15a49d6b15c2486032f5e0b50f43c11bc076c401e0779d224e33f6fc", - "0x5f70e3a2714d8b4483cf3155865ba792197e957f5b3a6234e4c408bf2e55119d", - "0x9b105b0f89a05eb1ff7caed74cf9573dc55ac8bc4881529487b3700f5842de16", - "0x1753571b3cfadca4277c59aee89f607d1b1e3a6aa515d9051bafb2f0d8ce0daa", - "0x4014fff940b0950706926a19906a370ccbd652836dab678c82c539c00989201a", - "0x0423fa59ee58035a0beb9653841036101b2d5903ddeabddabf697dbc6f168e61", - "0x78f6781673d991f9138aa1f5142214232d6e3d6986acb6cc7fb000e1a055f425", - "0x21b8a1f6733b5762499bf2de90c9ef06af1c6c8b3ddb3a04cce949caad723197", - "0x83847957e909153312b5bd9a1a37db0bd6c72a417024a69df3e18512973a18b4", - "0x948addf423afd0c813647cfe32725bc55773167d5065539e6a3b50e6ebbdab38", - "0x0b0485d1bec07504a2e5e3a89addd6f25d497cd37a0c04bc38355f8bdb01cd48", - "0x31be8bda5143d39ea2655e9eca6a294791ca7854a829904d8574bedc5057ddc4", - "0x16a0d2d657fadce0d81264320e42e504f4d39b931dff9888f861f3cc78753f99", - "0xb43786061420c5231bf1ff638cb210f89bf4cd2d3e8bafbf34f497c9a298a13b", - "0x1f5986cbd7107d2a3cbc1826ec6908d976addbf9ae78f647c1d159cd5397e1bd", - "0xa883ccdbfd91fad436be7a4e2e74b7796c0aadfe03b7eea036d492eaf74a1a6f", - "0x5bc9eb77bbbf589db48bca436360d5fc1d74b9195237f11946349951f2a9f7f6", - "0xb6bc86de74a887a5dceb012d58c62399897141cbcc51bad9cb882f53991f499c", - "0xa6c3260e7c2dd13f26cf22bf4cd667688142ff7a3511ec895bc8f92ebfa694b6", - "0xb97da27e17d26608ef3607d83634d6e55736af10cc7e4744940a3e35d926c2ad", - "0x9df44067c2dc947c2f8e07ecc90ba54db11eac891569061a8a8821f8f9773694", - "0x865cc98e373800825e2b5ead6c21ac9112ff25a0dc2ab0ed61b16dc30a4a7cd7", - "0xe06a5b157570c5e010a52f332cacd4e131b7aed9555a5f4b5a1c9c4606caca75", - "0x824eccb5cf079b5943c4d17771d7f77555a964a106245607cedac33b7a14922e", - "0xe86f721d7a3b52524057862547fc72de58d88728868f395887057153bccaa566", - "0x3344e76d79f019459188344fb1744c93565c7a35799621d7f4505f5b6119ac82", - "0x401b3589bdd1b0407854565329e3f22251657912e27e1fb2d978bf41c435c3ac", - "0xb12fd0b2567eb14a562e710a6e46eef5e280187bf1411f5573bb86ecbe05e328", - "0xe6dc27bab027cbd9fbb5d80054a3f25b576bd0b4902527a0fc6d0de0e45a3f9f", - "0x1de222f0e731001c60518fc8d2be7d7a48cc84e0570f03516c70975fdf7dc882", - "0xb8ff6563e719fc182e15bbe678cf045696711244aacc7ce4833c72d2d108b1b9", - "0x53e28ac2df219bcbbc9b90272e623d3f6ca3221e57113023064426eff0e2f4f2", - "0x8a4e0776f03819e1f35b3325f20f793d026ccae9a769d6e0f987466e00bd1ce7", - "0x2f65f20089a31f79c2c0ce668991f4440b576ecf05776c1f6abea5e9b14b570f", - "0x448e124079a48f62d0d79b96d5ed1ffb86610561b10d5c4236280b01f8f1f406", - "0x419b34eca1440c847f7bff9e948c9913075d8e13c270e67f64380a3f31de9bb2", - "0x2f6e4fee667acaa81ba8e51172b8329ed936d57e9756fb31f635632dbc2709b7", - "0xdd5afc79e8540fcee6a896c43887bd59c9de5d61b3d1b86539faeb41a14b251d", - "0xc707bed926a46cc451a6b05e642b6098368dbdbf14528c4c28733d5d005af516", - "0x153e850b606eb8a05eacecc04db4b560d007305e664bbfe01595cb69d26b8597", - "0x1b91cc07570c812bb329d025e85ef520132981337d7ffc3d84003f81a90bf7a7", - "0x4ca32e77a12951a95356ca348639ebc451170280d979e91b13316844f65ed42a", - "0xe49ea1998e360bd68771bd69c3cd4cf406b41ccca4386378bec66ea210c40084", - "0x01aaffbde1a672d253e0e317603c2dc1d0f752100d9e853f840bca96e57f314c", - "0x170d0befcbbaafb317c8684213a4989368332f66e889824cc4becf148f808146", - "0x56f973308edf5732a60aa3e7899ae1162c7a2c7b528c3315237e20f9125b34e0", - "0x66c54fd5f6d480cab0640e9f3ec1a4eafbafc0501528f57bb0d5c78fd03068ef", - "0xaca6c83f665c64d76fbc4858da9f264ead3b6ecdc3d7437bb800ef7240abffb9", - "0xf1d4e02e7c85a92d634d16b12dc99e1d6ec9eae3d8dfbca77e7c609e226d0ce7", - "0x094352545250e843ced1d3c6c7957e78c7d8ff80c470974778930adbe9a4ed1a", - "0x76efa93070d78b73e12eb1efa7f36d49e7944ddcc3a043b916466ee83dca52ce", - "0x1772a2970588ddb584eadf02178cdb52a98ab6ea8a4036d29e59f179d7ba0543", - "0xe4bbf2d97d65331ac9f680f864208a9074d1def3c2433458c808427e0d1d3167", - "0x8ccfb5252b22c77ea631e03d491ea76eb9b74bc02072c3749f3e9d63323b44df", - "0x9e212a9bdf4e7ac0730a0cecd0f6cc49afc7e3eca7a15d0f5f5a68f72e45363b", - "0x52e548ea6445aae3f75509782a7ab1f4f02c2a85cdd0dc928370f8c76ae8802d", - "0xb62e7d73bf76c07e1a6f822a8544b78c96a6ba4f5c9b792546d94b56ca12c8b9", - "0x595cb0e985bae9c59af151bc748a50923921a195bbec226a02157f3b2e066f5b", - "0x1c7aa6b36f402cec990bafefbdbb845fc6c185c7e08b6114a71dd388fe236d32", - "0x01ee2ff1a1e88858934a420258e9478585b059c587024e5ec0a77944821f798c", - "0x420a963a139637bffa43cb007360b9f7d305ee46b6a694b0db91db09618fc2e5", - "0x5a8e2ad20f8da35f7c885e9af93e50009929357f1f4b38a6c3073e8f58fae49e", - "0x52a405fdd84c9dd01d1da5e9d1c4ba95cb261b53bf714c651767ffa2f9e9ad81", - "0xa1a334c901a6d5adc8bac20b7df025e906f7c4cfc0996bfe2c62144691c21990", - "0xb789a00252f0b34bded3cb14ae969effcf3eb29d97b05a578c3be8a9e479c213", - "0xb9dbf7e9ddb638a515da245845bea53d07becdf3f8d1ec17de11d495624c8eab", - "0xaf566b41f5ed0c026fa8bc709533d3fa7a5c5d69b03c39971f32e14ab523fa3d", - "0x8121e0b2d9b106bb2aefd364fd6a450d88b88ee1f5e4aad7c0fcd8508653a112", - "0x8581c1be74279216b93e0a0d7272f4d6385f6f68be3eef3758d5f68b62ee7b6c", - "0x85386f009278f9a1f828404fa1bbfa02dfb9d896554f0a52678eb6ec8feadc55", - "0xf483ed167d92a0035ac65a1cfdb7906e4952f74ae3a1d86324d21f241daffcb7", - "0x3872485e2a520a350884accd990a1860e789dd0d0664ad14f50186a92c7be7be", - "0xc6c1a3301933019105f5650cabcb22bfbf221965ffcfc1329315b24ea3d77fd4", - "0xcee901330a60d212a867805ce0c28f53c6cc718f52156c9e74390d18f5df6280", - "0xa67ae793b1cd1a828a607bae418755c84dbb61adf00833d4c61a94665363284f", - "0x80d8159873b517aa6815ccd7c8ed7cfb74f84298d703a6c5a2f9d7d4d984ddde", - "0x1de5a8b915f2d9b45c97a8e134871e2effb576d05f4922b577ade8e3cd747a79", - "0x6ea17c5ece9b97dddb8b2101b923941a91e4b35e33d536ab4ff15b647579e1f5", - "0xcb78631e09bc1d79908ce1d3e0b6768c54b272a1a5f8b3b52485f98d6bba9245", - "0xd7c38f9d3ffdc626fe996218c008f5c69498a8a899c7fd1d63fbb03e1d2a073f", - "0x72cdef54267088d466244a92e4e6f10742ae5e6f7f6a615eef0da049a82068f9", - "0x60b3c490ba8c502656f9c0ed37c47283e74fe1bc7f0e9f651cbc76552a0d88eb", - "0x56bd0c66987a6f3761d677097be9440ea192c1cb0f5ec38f42789abe347e0ea9", - "0x3caac3e480f62320028f6f938ee147b4c78e88a183c464a0c9fb0df937ae30c1", - "0x7a4d2f11bddda1281aba5a160df4b814d23aef07669affe421a861fac2b4ec0f", - "0x9bb4d11299922dc309a4523959298a666ebe4063a9ee3bad1b93988ed59fb933", - "0x957323fffbaf8f938354662452115ae5acba1290f0d3f7b2a671f0359c109292", - "0x877624e31497d32e83559e67057c7a605fb888ed8e31ba68e89e02220eac7096", - "0x8456546ae97470ff6ea98daf8ae632e59b309bd3ff8e9211f7d21728620ed1e5", - "0xbacb26f574a00f466ce354e846718ffe3f3a64897d14d5ffb01afcf22f95e72b", - "0x0228743a6e543004c6617bf2c9a7eba1f92ebd0072fb0383cb2700c3aed38ba0", - "0x04f093f0f93c594549436860058371fb44e8daf78d6e5f563ba63a46b61ddbf0", - "0x0ba17c1ec93429ceaff08eb81195c9844821b64f2b5363926c2a6662f83fb930", - "0xd71605d8446878c677f146837090797e888416cfc9dc4e79ab11776cc6639d3f", - "0x33dde958dc5a6796138c453224d4d6e7f2ae740cceef3b52a8b669eb4b9691a1", - "0x3c39838295d1495e90e61ce59f6fcc693b31c292d02d31759719df6fe3214559", - "0x8aecc66f38644296cf0e6693863d57a243a31a4929130e22ab44cb6157b1af41", - "0xdf7153a7eab9521f2b37124067166c72de8f342249ac0e0f5350bd32f1251053", - "0xa498840b58897cf3bed3981b94c86d85536dfebbc437d276031ebd9352e171eb", - "0xb1df15a081042ab665458223a0449ffc71a10f85f3d977beb20380958fd92262", - "0x15d3bdbdee2a61b01d7a6b72a5482f6714358eedf4bece7bb8458e100caf8fba", - "0x0c96b7a0ea09c3ef758424ffb93654ce1520571e32e1f83aecbeded2388c3a7a", - "0xb4a3a8023266d141ecd7c8a7ca5282a825410b263bc11c7d6cab0587c9b5446e", - "0xf38f535969d9592416d8329932b3a571c6eacf1763de10fb7b309d3078b9b8d4", - "0x5a1e7b1c3b3943158341ce6d7f9f74ae481975250d89ae4d69b2fcd4c092eb4e", - "0xdad31e707d352f6cca78840f402f2ac9292094b51f55048abf0d2badfeff5463", - "0x097e290170068e014ceda3dd47b28ede57ff7f916940294a13c9d4aa2dc98aad", - "0x22e2dcedb6bb7f8ace1e43facaa502daa7513e523be98daf82163d2a76a1e0be", - "0x7ef2b211ab710137e3e8c78b72744bf9de81c2adde007aef6e9ce92a05e7a2c5", - "0x49b427805fc5186f31fdd1df9d4c3f51962ab74e15229e813072ec481c18c717", - "0xe60f6caa09fa803d97613d58762e4ff7f22f47d5c30b9d0116cdc6a357de4464", - "0xab3507b37ee92f026c72cc1559331630bc1c7335b374e4418d0d02687df1a9dd", - "0x50825ae74319c9adebc8909ed7fc461702db8230c59975e8add09ad5e7a647ab", - "0x0ee8e9c1d8a527a42fb8c2c8e9e51faf727cffc23ee22b5a95828f2790e87a29", - "0x675c21c290ddb40bec0302f36fbcd2d1832717a4bc05d113c6118a62bc8f9aca", - "0x580bafab24f673317b533148d7226d485e211eaa3d6e2be2529a83ca842b58a7", - "0x540e474776cae597af24c147dc1ae0f70a6233e98cf5c3ce31f38b830b75c99a", - "0x36eaf9f286e0f356eaaf8d81f71cc52c81d9ebc838c3b4859009f8567a224d16", - "0x0e2cbbb40954be047d02b1450a3dbd2350506448425dc25fd5faf3a66ee8f5c4", - "0x7eb0390cfe4c4eb120bbe693e87adc8ecab51d5fd8ce8f911c8ff07fad8cbe20", - "0xbf77589f5c2ebb465b8d7936f6260a18a243f59bd87390ee22cf579f6f020285", - "0x695b96bb28693f6928777591ef64146466d27521280a295936a52ec60707c565", - "0x22a0d018cbd4274caa8b9e7fb132e0a7ed787874046ca683a7d81d1c7c8b8f15", - "0x84092b122bb35e5ad85407b4b55f33707b86e0238c7970a8583f3c44308ed1d9", - "0xea346067ca67255235f9cae949f06e4b6c93846a7abc7c8c8cd786e9c4b3e4bc", - "0xa6df0716b125dc696b5d0e520cb49c1c089397c754efc146792e95bc58cc7159", - "0x7377b5d3953029fc597fb10bb6479ee34133d38f08783fbb61c7d070f34ea66f", - "0x7d79b00ffb976a10cd24476a394c8ed22f93837c51a58a3ddc7418153a5a8ea1", - "0x01e55182e80dff26cc3e06bb736b4a63745bde8ae28c604fa7fb97d99de5f416", - "0x062a2d5a207f8d540764d09648afecbf5033b13aec239f722b9033a762acf18b", - "0x48be60a3221d98b4d62f0b89d3bef74c70878dd65c6f79b34c2c36d0ddaa1da0", - "0x41e11f33543cf045c1a99419379ea31523d153bdf664549286b16207b9648c85", - "0xeef4d30b4700813414763a199e7cc6ab0faec65ef8b514faa01c6aa520c76334", - "0xea7cfe990422663417715e7859fc935ca47f47c943a1254044b6bc5934c94bc8", - "0xbbd3c834e5403b98a0ca346c915a23310f3d58880786628bc6cfbe05ba29c3c5", - "0xe216379f385bc9995ae0f37f1409a78d475c56b8aeb4ee434326724ec20124f7", - "0xdd328a1eee19d09b6fef06e252f8ad0ae328fbf900ef745f5950896803a3899d", - "0xa16fde34b0d743919feb0781eca0c525a499d279119af823cb3a8817000335db", - "0x7a28d108c59b83b12c85cd9aabc1d1d994a9a0329ae7b64a32aadcd61ebe50e3", - "0xb28bc82fceae74312eb837a805f0a8a01c0f669b99bb03fde31c4d58bedff89b", - "0x1b0d8f37d349781e846900b51a90c828aa384afe9b8ee1f88aeb8dba4b3168f2", - "0xbfd0301ff964c286c3331a30e09e0916da6f484e9c9596dbf1cae3cc902dbf9e", - "0xbb8254cb9ef6b485b8fb6caeafe45f920affc30f6b9d671e9a454530536f4fef", - "0xcad2317cf63dfa7147ded5c7e15f5f72e78f42d635e638f1ece6bc722ca3638b", - "0xb6c6e856fd45117f54775142f2b38f31114539d8943bcbcf823f6c7650c001e4", - "0x869f1baa35684c8f67a5bc99b294187852e6c85243a2f36481d0891d8b043020", - "0x14c6ccf145ee40ff56e3810058d2fba9a943ffc7c7087c48a08b2451c13dc788", - "0x263c1bcb712890f155b7e256cefa4abf92fe4380f3ffc11c627d5e4e30864d18", - "0x69f4eaf655e31ad7f7a725cd415ce7e45dd4a8396ac416950d42ed33155c3487", - "0x47e8eec2c5e33c9a54fe1f9b09e7744b614fb16531c36b862aa899424be13b05", - "0x5c985de270e62c44f0b49157882e8e83641b906ce47959e337fe8423e125a2eb", - "0x4e13b11e13202439bb5de5eea3bb75d2d7bf90f91411163ade06161a9cf424db", - "0x583a8fa159bb74fa175d72f4e1705e9a3b8ffe26ec5ad6e720444b99288f1213", - "0x903d2a746a98dfe2ee2632606d57a9b0fa6d8ccd895bb18c2245fd91f8a43676", - "0xa35a51330316012d81ec7249e3f2b0c9d7fcbb99dd98c62fe880d0a152587f51", - "0x33818a7beb91730c7b359b5e23f68a27b429967ea646d1ea99c314353f644218", - "0x183650af1e0b67f0e7acb59f8c72cc0e60acc13896184db2a3e4613f65b70a8b", - "0x857ff2974bef960e520937481c2047938a718cea0b709282ed4c2b0dbe2ef8fa", - "0x95a367ecb9a401e98a4f66f964fb0ece783da86536410a2082c5dbb3fc865799", - "0x56c606a736ac8268aedadd330d2681e7c7919af0fe855f6c1c3d5c837aa92338", - "0x5c97f7abf30c6d0d4c23e762c026b94a6052a444df4ed942e91975419f68a3a4", - "0x0b571de27d2022158a3128ae44d23a8136e7dd2dee74421aa4d6ed15ee1090a0", - "0xa17f6bc934a2f3c33cea594fee8c96c1290feec934316ebbbd9efab4937bf9f9", - "0x9ff57d70f27aad7281841e76435285fd27f10dad256b3f5cabde4ddc51b70eff", - "0xafa3071a847215b3ccdf51954aa7cb3dd2e6e2a39800042fc42009da705508b2", - "0x5e3bea33e4ac6f7c50a077d19571b1796e403549b1ce7b15e09905a0cc5a4acf", - "0x0dc7ba994e632ab95f3ecb7848312798810cf761d1c776181882d17fd6dda075", - "0xb4f7158679dad9f7370a2f64fbe617a40092849d17453b4f50a93ca8c6885844", - "0x094564b00f53c6f27c121fd8adfe1685b258b259e585a67b57c85efb804c57b2", - "0x9cd21a4249ba3fccffad550cdb8409dc12d8b74a7192874b6bafe2363886f318", - "0xbb22e0dad55cb315c564c038686419d40ef7f13af2143a28455bf445f6e10393", - "0x2a71d5e00821178c2cd39e7501e07da5cca6680eb7cdbe996f52dccafadb3735", - "0x9619406093b121e044a5b403bb1713ae160aeb52ad441f82dc6c63e4b323b969", - "0x3b8bd1d82c6d67ae707e19b889f1cb1f7bba912f12ae4284298f3a70c3644c79", - "0xd7a70c50d47d48785b299dbea01bf03ef18b8495de3c35cb265bc8f3295c4e15", - "0x8802ecce8dd6b6190af8ac79aafda3479c29f548d65e5798c0ca51a529b19108", - "0x4b630e1df52ec5fd650f4a4e76b3eeddda39e1e9eab996f6d3f02eefdf690990", - "0x0bfbff60fcf7f411d469f7f6f0a58ca305fd84eb529ee3ac73c00174793d723e", - "0x535f78b5f3a99a1c498e2c19dc1acb0fbbaba8972ba1d7d66936c28ab3667ebe", - "0x06ba92d8129db98fec1b75f9489a394022854f22f2e9b9450b187a6fc0d94a86", - "0xb7ae275ba10f80fb618a2cf949d5ad2e3ae24eb2eb37dcf1ec8c8b148d3ba27f", - "0xb275579bcf2584d9794dd3fc7f999902b13d33a9095e1980d506678e9c263de1", - "0x843ccd52a81e33d03ad2702b4ef68f07ca0419d4495df848bff16d4965689e48", - "0xde8b779ca7250f0eb867d5abdffd1d28c72a5a884d794383fc93ca40e5bf6276", - "0x6b789a2befccb8788941c9b006e496b7f1b03dbb8e530ba339db0247a78a2850", - "0xfccd4dca80bc52f9418f26b0528690255e320055327a34b50caf088235d2f660", - "0x18479ebfbe86c1e94cd05c70cb6cace6443bd9fdac7e01e9c9535a9e85141f2f", - "0x5350c8f3296441db954a261238c88a3a0c51ab418a234d566985f2809e211148", - "0xa5636614135361d03a381ba9f6168e2fd0bd2c1105f9b4e347c414df8759dea3", - "0xe7bb69e600992e6bd41c88a714f50f450153f1a05d0ddb4213a3fc4ba1f48c3f", - "0x17b42e81bae19591e22aa2510be06803bcb5c39946c928c977d78f346d3ca86b", - "0x30a10c07dc9646b7cbb3e1ab722a94d2c53e04c0c19efaaea7dccba1b00f2a20", - ], - compressed_lamport_pk: - "0x672ba456d0257fe01910d3a799c068550e84881c8d441f8f5f833cbd6c1a9356", - child_sk: - "7419543105316279183937430842449358701327973165530407166294956473095303972104" + seed: "0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + master_sk: + "6083874454709270928345386274498605044986640685124978867557563392430687146096", + child_index: 0, + lamport_0: vec![ + "0xe345d0ad7be270737de05cf036f688f385d5f99c7fddb054837658bdd2ebd519", + "0x65050bd4db9c77c051f67dcc801bf1cdf33d81131e608505bb3e4523868eb76c", + "0xc4f8e8d251fbdaed41bdd9c135b9ed5f83a614f49c38fffad67775a16575645a", + "0x638ad0feace7567255120a4165a687829ca97e0205108b8b73a204fba6a66faa", + "0xb29f95f64d0fcd0f45f265f15ff7209106ab5f5ce6a566eaa5b4a6f733139936", + "0xbcfbdd744c391229f340f02c4f2d092b28fe9f1201d4253b9045838dd341a6bf", + "0x8b9cf3531bfcf0e4acbfd4d7b4ed614fa2be7f81e9f4eaef53bedb509d0b186f", + "0xb32fcc5c4e2a95fb674fa629f3e2e7d85335f6a4eafe7f0e6bb83246a7eced5f", + "0xb4fe80f7ac23065e30c3398623b2761ac443902616e67ce55649aaa685d769ce", + "0xb99354f04cfe5f393193c699b8a93e5e11e6be40ec16f04c739d9b58c1f55bf3", + "0x93963f58802099ededb7843219efc66a097fab997c1501f8c7491991c780f169", + "0x430f3b027dbe9bd6136c0f0524a0848dad67b253a11a0e4301b44074ebf82894", + "0xd635c39b4a40ad8a54d9d49fc8111bd9d11fb65c3b30d8d3eaef7d7556aac805", + "0x1f7253a6474cf0b2c05b02a7e91269137acddedcb548144821f9a90b10eccbab", + "0x6e3bdb270b00e7b6eb8b044dbfae07b51ea7806e0d24218c59a807a7fd099c18", + "0x895488ad2169d8eaae332ce5b0fe1e60ffab70e62e1cb15a2a1487544af0a6e8", + "0x32d45a99d458c90e173a3087ea3661ab62d429b285089e92806a9663ba825342", + "0xc15c52106c3177f5848a173076a20d46600ca65958a1e3c7d45a593aaa9670ed", + "0xd8180c550fbe4cd6d5b676ff75e0728729d8e28a3b521d56152594ac6959d563", + "0x58fe153fac8f4213aaf175e458435e06304548024bcb845844212c774bdffb2a", + "0x10fff610a50f4bee5c978f512efa6ab4fafacb65929606951ba5b93eeb617b5a", + "0x78ac9819799b52eba329f13dd52cf0f6148a80bf04f93341814c4b47bb4aa5ec", + "0xa5c3339caa433fc11e74d1765bec577a13b054381a44b23c2482e750696876a9", + "0x9f716640ab5cdc2a5eb016235cddca2dc41fa4ec5acd7e58af628dade99ec376", + "0x2544364320e67577c4fed8c7c7c839deed93c24076d5343c5b8faca4cc6dc2d8", + "0x62553e782541f822c589796be5d5c83bfc814819100b2be0710b246f5aa7149c", + "0x229fb761c46c04b22ba5479f2696be0f936fded68d54dd74bcd736b8ba512afb", + "0x0af23996a65b98a0ebaf19f3ec0b3ef20177d1bfd6eb958b3bd36e0bdbe04c8c", + "0x6f0954f9deab52fd4c8d2daba69f73a80dea143dd49d9705c98db3d653adf98c", + "0xfa9221dd8823919a95b35196c1faeb59713735827f3e84298c25c83ac700c480", + "0x70c428e3ff9e5e3cda92d6bb85018fb89475c19f526461cca7cda64ebb2ff544", + "0xdcaac3413e22314f0f402f8058a719b62966b3a7429f890d947be952f2e314ba", + "0xb6b383cb5ec25afa701234824491916bfe6b09d28cf88185637e2367f0cf6edc", + "0x7b0d91488fc916aba3e9cb61a5a5645b9def3b02e4884603542f679f602afb8d", + "0xe9c20abca284acfde70c59584b9852b85c52fa7c263bb981389ff8d638429cd7", + "0x838524f798daee6507652877feb9597f5c47e9bb5f9aa52a35fb6fff796813b9", + "0xbe1ca18faf9bf322474fad1b3d9b4f1bc76ae9076e38e6dd2b16e2faf487742b", + "0xbf02d70f1a8519343a16d24bade7f7222912fd57fe4f739f367dfd99d0337e8e", + "0xc979eb67c107ff7ab257d1c0f4871adf327a4f2a69e01c42828ea27407caf058", + "0xf769123d3a3f19eb7b5c3fd4f467a042944a7c5ff8834cebe427f47dbd71460c", + "0xaefc8edc23257e1168a35999fe3832bcbc25053888cc89c38667482d6748095b", + "0x8ff399f364d3a2428b1c92213e4fdc5341e7998007da46a5a2f671929b42aaab", + "0xcf2a3d9e6963b24c5001fbba1e5ae7f45dd6cf520fd24861f745552db86bab48", + "0xb380e272d7f3091e5c887fa2e7c690c67d59f4d95f8376d150e555da8c738559", + "0xc006a749b091d91204dbb64f59059d284899de5986a7f84f8877afd5e0e4c253", + "0x818d8bb9b7da2dafa2ef059f91975e7b6257f5e199d217320de0a576f020de5c", + "0x7aabf4a1297d2e550a2ee20acb44c1033569e51b6ec09d95b22a8d131e30fd32", + "0xdd01c80964a5d682418a616fb10810647c9425d150df643c8ddbbe1bfb2768b7", + "0x1e2354e1d97d1b06eb6cfe9b3e611e8d75b5c57a444523e28a8f72a767eff115", + "0x989c9a649dca0580256113e49ea0dd232bbfd312f68c272fe7c878acc5da7a2c", + "0x14ee1efe512826fff9c028f8c7c86708b841f9dbf47ce4598298b01134ebdc1a", + "0x6f861dba4503f85762d9741fa8b652ce441373f0ef2b7ebbd5a794e48cdab51b", + "0xda110c9492ffdb87efe790214b7c9f707655a5ec08e5af19fb2ab2acc428e7dc", + "0x5576aa898f6448d16e40473fcb24c46c609a3fc46a404559faa2d0d34d7d49ce", + "0x9bd9a35675f2857792bc45893655bfdf905ffeaee942d93ad39fbcadd4ca9e11", + "0xfa95e4c37db9303d5213890fd984034089cbc9c6d754741625da0aa59cc45ccf", + "0xfef7d2079713f17b47239b76c8681bf7f800b1bfeac7a53265147579572ddf29", + "0x39aa7c0fecf9a1ed037c685144745fda16da36f6d2004844cf0e2d608ef6ed0e", + "0x5530654d502d6ba30f2b16f49cc5818279697308778fd8d40db8e84938144fb6", + "0xb1beaa36397ba1521d7bf7df16536969d8a716e63510b1b82a715940180eb29f", + "0x21abe342789f7c15a137afa373f686330c0db8c861572935a3cd8dcf9e4e1d45", + "0x27b5a1acda55b4e0658887bd884d3203696fcae0e94f19e31bfe931342b1c257", + "0x58401a02502d7708a812c0c72725f768f5a556480517258069f2d72543cda888", + "0x4b38f291548f51bee7e4cf8cc5c8aa8f4ad3ec2461dba4ccbab70f1c1bfd7feb", + "0x9b39a53fdafaaf1d23378e0aa8ae65d38480de69821de2910873eefc9f508568", + "0x932200566a3563ee9141913d12fd1812cb008cb735724e8610890e101ec10112", + "0x6a72f70b4ec5491f04780b17c4776a335fcc5bff5073d775150e08521dc74c91", + "0x86d5c60e627a4b7d5d075b0ba33e779c45f3f46d22ed51f31360afd140851b67", + "0x5ca2a736bb642abc4104faa781c9aff13d692a400d91dc961aec073889836946", + "0xa14bca5a262ac46ceac21388a763561fc85fb9db343148d786826930f3e510cd", + "0x87be03a87a9211504aa70ec149634ee1b97f7732c96377a3c04e98643dcba915", + "0x8fe283bc19a377823377e9c326374ebb3f29527c12ea77bfb809c18eef8943b0", + "0x8f519078b39a3969f7e4caeca9839d4e0eccc883b89e4a86d0e1731bfc5e33fc", + "0x33d7c28c3d26fdfc015a8c2131920e1392ef0aea55505637b54ea63069c7858e", + "0xe57de7c189fcc9170320c7acedb38798562a48dbc9943b2a8cd3441d58431128", + "0x513dac46017050f82751a07b6c890f14ec43cadf687f7d202d2369e35b1836b4", + "0xfd967d9f805bb7e78f7b7caa7692fdd3d6b5109c41ad239a08ad0a38eeb0ac4c", + "0xf2013e4da9abcc0f03ca505ed94ec097556dbfd659088cd24ec223e02ac43329", + "0xe0dcfac50633f7417f36231df2c81fa1203d358d5f57e896e1ab4b512196556b", + "0xf022848130e73fe556490754ef0ecfcdaaf3b9ff16ae1eda7d38c95c4f159ded", + "0x2147163a3339591ec7831d2412fb2d0588c38da3cd074fa2a4d3e5d21f9f1d2d", + "0x11ee2404731962bf3238dca0d9759e06d1a5851308b4e6321090886ec5190b69", + "0xf7679ecd07143f8ac166b66790fa09aed39352c09c0b4766bbe500b1ebace5a5", + "0xc7a0e95f09076472e101813a95e6ea463c35bd5ee9cfda3e5d5dbccb35888ef0", + "0xde625d3b547eb71bea5325a0191a592fa92a72e4b718a499fdba32e245ddf37e", + "0x7e5bdccd95df216e8c59665073249072cb3c9d0aef6b341afc0ca90456942639", + "0xc27f65fd9f797ede374e06b4ddb6e8aa59c7d6f36301f18b42c48b1889552fe3", + "0x8175730a52ea571677b035f8e2482239dda1cfbff6bc5cde00603963511a81af", + "0x09e440f2612dad1259012983dc6a1e24a73581feb1bd69d8a356eea16ba5fd0e", + "0x59dcc81d594cbe735a495e38953e8133f8b3825fd84767af9e4ea06c49dbabfa", + "0x6c8480b59a1a958c434b9680edea73b1207077fb9a8a19ea5f9fbbf6f47c4124", + "0x81f5c89601893b7a5a231a7d37d6ab9aa4c57f174fcfc6b40002fa808714c3a1", + "0x41ba4d6b4da141fcc1ee0f4b47a209cfd143d34e74fc7016e9956cedeb2db329", + "0x5e0b5b404c60e9892040feacfb4a84a09c2bc4a8a5f54f3dad5dca4acdc899dc", + "0xe922eebf1f5f15000d8967d16862ed274390cde808c75137d2fb9c2c0a80e391", + "0xbf49d31a59a20484f0c08990b2345dfa954509aa1f8901566ab9da052b826745", + "0xb84e07da828ae668c95d6aa31d4087504c372dbf4b5f8a8e4ded1bcf279fd52b", + "0x89288bf52d8c4a9561421ad199204d794038c5d19ae9fee765ee2b5470e68e7e", + "0xf6f618be99b85ec9a80b728454a417c647842215e2160c6fe547dd5a69bd9302", + "0xdd9adc002f98c9a47c7b704fc0ce0a5c7861a5e2795b6014749cde8bcb8a034b", + "0xd119a4b2c0db41fe01119115bcc35c4b7dbfdb42ad3cf2cc3f01c83732acb561", + "0x9c66bc84d416b9193bad9349d8c665a9a06b835f82dc93ae0cccc218f808aad0", + "0xd4b50eefcd2b5df075f14716cf6f2d26dfc8ae02e3993d711f4a287313038fde", + "0xaf72bfb346c2f336b8bc100bff4ba35d006a3dad1c5952a0adb40789447f2704", + "0xc43ca166f01dc955e7b4330227635feb1b0e0076a9c5633ca5c614a620244e5b", + "0x5efca76970629521cfa053fbbbda8d3679cadc018e2e891043b0f52989cc2603", + "0x35c57de1c788947f187051ce032ad1e899d9887d865266ec6fcfda49a8578b2b", + "0x56d4be8a65b257216eab7e756ee547db5a882b4edcd12a84ed114fbd4f5be1f1", + "0x257e858f8a4c07a41e6987aabaa425747af8b56546f2a3406f60d610bcc1f269", + "0x40bd9ee36d52717ab22f1f6b0ee4fb38b594f58399e0bf680574570f1b4b8c90", + "0xcb6ac01c21fc288c12973427c5df6eb8f6aefe64b92a6420c6388acdf36bc096", + "0xa5716441312151a5f0deb52993a293884c6c8f445054ce1e395c96adeee66c6d", + "0xe15696477f90113a10e04ba8225c28ad338c3b6bdd7bdeb95c0722921115ec85", + "0x8faeaa52ca2f1d791cd6843330d16c75eaf6257e4ba236e3dda2bc1a644aee00", + "0xc847fe595713bf136637ce8b43f9de238762953fed16798878344da909cc76ae", + "0xb5740dc579594dd110078ce430b9696e6a308078022dde2d7cfe0ef7647b904e", + "0x551a06d0771fcd3c53aea15aa8bf700047138ef1aa22265bee7fb965a84c9615", + "0x9a65397a5907d604030508d41477de621ce4a0d79b772e81112d634455e7a4da", + "0x6462d4cc2262d7faf8856812248dc608ae3d197bf2ef410f00c3ae43f2040995", + "0x6782b1bd319568e30d54b324ab9ed8fdeac6515e36b609e428a60785e15fb301", + "0x8bcdcf82c7eb2a07e14db20d80d9d2efea8d40320e121923784c92bf38250a8e", + "0x46ed84fa17d226d5895e44685747ab82a97246e97d6237014611aaaba65ed268", + "0x147e87981673326c5a2bdb06f5e90eaaa9583857129451eed6dde0c117fb061f", + "0x4141d6fe070104c29879523ba6669552f3d457c0929bb878d2751f4ff059b895", + "0xd866ce4ef226d74841f950fc28cdf2235db21e0e3f07a0c8f807704464db2210", + "0xa804f9118bf92558f684f90c2bda832a4f51ef771ffb2765cde3ec6f48124f32", + "0xc436d4a65910124e00cded9a637178914a8fbc090400f3f031c03eac4d0295a5", + "0x643fdb9243656512316528de04dcc7344ca33783580ad0c3debf8c4a6e7c8bc4", + "0x7f4a345b41706b281b2de998e91ff62d908eb29fc333ee336221757753c96e23", + "0x6bdc086a5b11de950cabea33b72d98db886b291c4c2f02d3e997edc36785d249", + "0xfb10b5b47d374078c0a52bff7174bf1cd14d872c7d20b4a009e2afd3017a9a17", + "0x1e07e605312db5380afad8f3d7bd602998102fdd39565b618ac177b13a6527e6", + "0xc3161b5a7b93aabf05652088b0e5b4803a18be693f590744c42c24c7aaaeef48", + "0xa47e4f25112a7d276313f153d359bc11268b397933a5d5375d30151766bc689a", + "0xb24260e2eff88716b5bf5cb75ea171ac030f5641a37ea89b3ac45acb30aae519", + "0x2bcacbebc0a7f34406db2c088390b92ee34ae0f2922dedc51f9227b9afb46636", + "0xc78c304f6dbe882c99c5e1354ce6077824cd42ed876db6706654551c7472a564", + "0x6e2ee19d3ee440c78491f4e354a84fa593202e152d623ed899e700728744ac85", + "0x2a3f438c5dc012aa0997b66f661b8c10f4a0cd7aa5b6e5922b1d73020561b27f", + "0xd804f755d93173408988b95e9ea0e9feae10d404a090f73d9ff84df96f081cf7", + "0xe06fda941b6936b8b33f00ffa02c8b05fd78fbec953da61da2043f5644b30a50", + "0x45ee279b465d53148850a16cc7f6bd33e7627aef554a9418ed012ca8f9717f80", + "0x9c79348c1bcd6aa2135452491d73564413a247ea8cc38fa7dcc6c43f8a2d61d5", + "0x7c91e056f89f2a77d3e3642e595bcf4973c3bca68dd2b10f51ca0d8945e4255e", + "0x669f976ebe38cbd22c5b1f785e14b76809d673d2cb1458983dbda41f5adf966b", + "0x8bc71e99ffcc119fd8bd604af54c0663b0325a3203a214810fa2c588089ed5a7", + "0x36b3f1ffeae5d9855e0965eef33f4c5133d99685802ac5ce5e1bb288d308f889", + "0x0aad33df38b3f31598e04a42ec22f20bf2e2e9472d02371eb1f8a06434621180", + "0x38c5632b81f90efbc51a729dcae03626a3063aa1f0a102fd0e4326e86a08a732", + "0x6ea721753348ed799c98ffa330d801e6760c882f720125250889f107915e270a", + "0xe700dd57ce8a653ce4269e6b1593a673d04d3de8b79b813354ac7c59d1b99adc", + "0xe9294a24b560d62649ca898088dea35a644d0796906d41673e29e4ea8cd16021", + "0xf20bb60d13a498a0ec01166bf630246c2f3b7481919b92019e2cfccb331f2791", + "0xf639a667209acdd66301c8e8c2385e1189b755f00348d614dc92da14e6866b38", + "0x49041904ee65c412ce2cd66d35570464882f60ac4e3dea40a97dd52ffc7b37a2", + "0xdb36b16d3a1010ad172fc55976d45df7c03b05eab5432a77be41c2f739b361f8", + "0x71400cdd2ea78ac1bf568c25a908e989f6d7e2a3690bc869c7c14e09c255d911", + "0xf0d920b2d8a00b88f78e7894873a189c580747405beef5998912fc9266220d98", + "0x1a2baefbbd41aa9f1cc5b10e0a7325c9798ba87de6a1302cf668a5de17bc926a", + "0x449538a20e52fd61777c45d35ff6c2bcb9d9165c7eb02244d521317f07af6691", + "0x97006755b9050b24c1855a58c4f4d52f01db4633baff4b4ef3d9c44013c5c665", + "0xe441363a27b26d1fff3288222fa8ed540f8ca5d949ddcc5ff8afc634eec05336", + "0xed587aa8752a42657fea1e68bc9616c40c68dcbbd5cb8d781e8574043e29ef28", + "0x47d896133ba81299b8949fbadef1c00313d466827d6b13598685bcbb8776c1d2", + "0x7786bc2cb2d619d07585e2ea4875f15efa22110e166af87b29d22af37b6c047d", + "0x956b76194075fe3daf3ca508a6fad161deb05d0026a652929e37c2317239cbc6", + "0xec9577cb7b85554b2383cc4239d043d14c08d005f0549af0eca6994e203cb4e7", + "0x0722d0c68d38b23b83330b972254bbf9bfcf32104cc6416c2dad67224ac52887", + "0x532b19d54fb6d77d96452d3e562b79bfd65175526cd793f26054c5f6f965df39", + "0x4d62e065e57cbf60f975134a360da29cabdcea7fcfc664cf2014d23c733ab3b4", + "0x09be0ea6b363fd746b303e482cb4e15ef25f8ae57b7143e64cbd5c4a1d069ebe", + "0x69dcddc3e05147860d8d0e90d602ac454b609a82ae7bb960ee2ecd1627d77777", + "0xa5e2ae69d902971000b1855b8066a4227a5be7234ac9513b3c769af79d997df4", + "0xc287d4bc953dcff359d707caf2ccba8cc8312156eca8aafa261fb72412a0ea28", + "0xb27584fd151fb30ed338f9cba28cf570f7ca39ebb03eb2e23140423af940bd96", + "0x7e02928194441a5047af89a6b6555fea218f1df78bcdb5f274911b48d847f5f8", + "0x9ba611add61ea6ba0d6d494c0c4edd03df9e6c03cafe10738cee8b7f45ce9476", + "0x62647ec3109ac3db3f3d9ea78516859f0677cdde3ba2f27f00d7fda3a447dd01", + "0xfa93ff6c25bfd9e17d520addf5ed2a60f1930278ff23866216584853f1287ac1", + "0x3b391c2aa79c2a42888102cd99f1d2760b74f772c207a39a8515b6d18e66888a", + "0xcc9ae3c14cbfb40bf01a09bcde913a3ed208e13e4b4edf54549eba2c0c948517", + "0xc2b8bce78dd4e876da04c54a7053ca8b2bedc8c639cee82ee257c754c0bea2b2", + "0xdb186f42871f438dba4d43755c59b81a6788cb3b544c0e1a3e463f6c2b6f7548", + "0xb7f8ba137c7783137c0729de14855e20c2ac4416c33f5cac3b235d05acbab634", + "0x282987e1f47e254e86d62bf681b0803df61340fdc9a8cf625ef2274f67fc6b5a", + "0x04aa195b1aa736bf8875777e0aebf88147346d347613b5ab77bef8d1b502c08c", + "0x3f732c559aee2b1e1117cf1dec4216a070259e4fa573a7dcadfa6aab74aec704", + "0x72699d1351a59aa73fcede3856838953ee90c6aa5ef5f1f7e21c703fc0089083", + "0x6d9ce1b8587e16a02218d5d5bed8e8d7da4ac40e1a8b46eeb412df35755c372c", + "0x4f9c19b411c9a74b8616db1357dc0a7eaf213cb8cd2455a39eb7ae4515e7ff34", + "0x9163dafa55b2b673fa7770b419a8ede4c7122e07919381225c240d1e90d90470", + "0x268ff4507b42e623e423494d3bb0bc5c0917ee24996fb6d0ebedec9ce8cd9d5c", + "0xff6e6169d233171ddc834e572024586eeb5b1bda9cb81e5ad1866dbc53dc75fe", + "0xb379a9c8279205e8753b6a5c865fbbf70eb998f9005cd7cbde1511f81aed5256", + "0x3a6b145e35a592e037c0992c9d259ef3212e17dca81045e446db2f3686380558", + "0x60fb781d7b3137481c601871c1c3631992f4e01d415841b7f5414743dcb4cfd7", + "0x90541b20b0c2ea49bca847e2db9b7bba5ce15b74e1d29194a12780e73686f3dd", + "0xe2b0507c13ab66b4b769ad1a1a86834e385b315da2f716f7a7a8ff35a9e8f98c", + "0xeefe54bc9fa94b921b20e7590979c28a97d8191d1074c7c68a656953e2836a72", + "0x8676e7f59d6f2ebb0edda746fc1589ef55e07feab00d7008a0f2f6f129b7bb3a", + "0x78a3d93181b40152bd5a8d84d0df7f2adde5db7529325c13bc24a5b388aed3c4", + "0xcc0e2d0cba7aaa19c874dbf0393d847086a980628f7459e9204fda39fad375c0", + "0x6e46a52cd7745f84048998df1a966736d2ac09a95a1c553016fef6b9ec156575", + "0x204ac2831d2376d4f9c1f5c106760851da968dbfc488dc8a715d1c764c238263", + "0xbdb8cc7b7e5042a947fca6c000c10b9b584e965c3590f92f6af3fe4fb23e1358", + "0x4a55e4b8a138e8508e7b11726f617dcf4155714d4600e7d593fd965657fcbd89", + "0xdfe064bb37f28d97b16d58b575844964205e7606dce914a661f2afa89157c45b", + "0x560e374fc0edda5848eef7ff06471545fcbdd8aefb2ecddd35dfbb4cb03b7ddf", + "0x10a66c82e146da5ec6f48b614080741bc51322a60d208a87090ad7c7bf6b71c6", + "0x62534c7dc682cbf356e6081fc397c0a17221b88508eaeff798d5977f85630d4f", + "0x0138bba8de2331861275356f6302b0e7424bbc74d88d8c534479e17a3494a15b", + "0x580c7768bf151175714b4a6f2685dc5bcfeb088706ee7ed5236604888b84d3e4", + "0xd290adb1a5dfc69da431c1c0c13da3be788363238d7b46bc20185edb45ab9139", + "0x1689879db6c78eb4d3038ed81be1bc106f8cfa70a7c6245bd4be642bfa02ebd7", + "0x6064c384002c8b1594e738954ed4088a0430316738def62822d08b2285514918", + "0x01fd23493f4f1cc3c5ff4e96a9ee386b2a144b50a428a6b5db654072bddadfe7", + "0xd5d05bb7f23ab0fa2b82fb1fb14ac29c2477d81a85423d0a45a4b7d5bfd81619", + "0xd72b9a73ae7b24db03b84e01106cea734d4b9d9850b0b7e9d65d6001d859c772", + "0x156317cb64578db93fee2123749aff58c81eae82b189b0d6f466f91de02b59df", + "0x5fba299f3b2c099edbac18d785be61852225890fc004bf6be0787d62926a79b3", + "0x004154f28f685bdbf0f0d6571e7a962a4c29b6c3ebedaaaf66097dfe8ae5f756", + "0x4b45816f9834c3b289affce7a3dc80056c2b7ffd3e3c250d6dff7f923e7af695", + "0x6ca53bc37816fff82346946d83bef87860626bbee7fd6ee9a4aeb904d893a11f", + "0xf48b2f43184358d66d5b5f7dd2b14a741c7441cc7a33ba3ebcc94a7b0192d496", + "0x3cb98f4baa429250311f93b46e745174f65f901fab4eb8075d380908aaaef650", + "0x343dfc26b4473b3a20e706a8e87e5202a4e6b96b53ed448afb9180c3f766e5f8", + "0x1ace0e8a735073bcbaea001af75b681298ef3b84f1dbab46ea52cee95ab0e7f9", + "0xd239b110dd71460cdbc41ddc99494a7531186c09da2a697d6351c116e667733b", + "0x22d6955236bd275969b8a6a30c23932670a6067f68e236d2869b6a8b4b493b83", + "0x53c1c01f8d061ac89187e5815ef924751412e6a6aa4dc8e3abafb1807506b4e0", + "0x2f56dd20c44d7370b713e7d7a1bfb1a800cac33f8a6157f278e17a943806a1f7", + "0xc99773d8a5b3e60115896a65ac1d6c15863317d403ef58b90cb89846f4715a7f", + "0x9f4b6b77c254094621cd336da06fbc6cbb7b8b1d2afa8e537ceca1053c561ef5", + "0x87944d0b210ae0a6c201cba04e293f606c42ebaed8b4a5d1c33f56863ae7e1b5", + "0xa7d116d962d03ca31a455f9cda90f33638fb36d3e3506605aa19ead554487a37", + "0x4042e32e224889efd724899c9edb57a703e63a404129ec99858048fbc12f2ce0", + "0x36759f7a0faeea1cd4cb91e404e4bf09908de6e53739603d5f0db52b664158a3", + "0xa4d50d005fb7b9fea8f86f1c92439cc9b8446efef7333ca03a8f6a35b2d49c38", + "0x80cb7c3e20f619006542edbe71837cdadc12161890a69eea8f41be2ee14c08a3", + "0xbb3c44e1df45f2bb93fb80e7f82cee886c153ab484c0095b1c18df03523629b4", + "0x04cb749e70fac3ac60dea779fceb0730b2ec5b915b0f8cf28a6246cf6da5db29", + "0x4f5189b8f650687e65a962ef3372645432b0c1727563777433ade7fa26f8a728", + "0x322eddddf0898513697599b68987be5f88c0258841affec48eb17cf3f61248e8", + "0x6416be41cda27711d9ec22b3c0ed4364ff6975a24a774179c52ef7e6de9718d6", + "0x0622d31b8c4ac7f2e30448bdadfebd5baddc865e0759057a6bf7d2a2c8b527e2", + "0x40f096513588cc19c08a69e4a48ab6a43739df4450b86d3ec2fb3c6a743b5485", + "0x09fcf7d49290785c9ea2d54c3d63f84f6ea0a2e9acfcdbb0cc3a281ce438250e", + "0x2000a519bf3da827f580982d449b5c70fcc0d4fa232addabe47bb8b1c471e62e", + "0xf4f80008518e200c40b043f34fb87a6f61b82f8c737bd784292911af3740245e", + "0x939eaab59f3d2ad49e50a0220080882319db7633274a978ced03489870945a65", + "0xadcad043d8c753fb10689280b7670f313253f5d719039e250a673d94441ee17c", + "0x58b7b75f090166b8954c61057074707d7e38d55ce39d9b2251bbc3d72be458f8", + "0xf61031890c94c5f87229ec608f2a9aa0a3f455ba8094b78395ae312cbfa04087", + "0x356a55def50139f94945e4ea432e7a9defa5db7975462ebb6ca99601c614ea1d", + "0x65963bb743d5db080005c4db59e29c4a4e86f92ab1dd7a59f69ea7eaf8e9aa79", + ], + lamport_1: vec![ + "0x9c0bfb14de8d2779f88fc8d5b016f8668be9e231e745640096d35dd5f53b0ae2", + "0x756586b0f3227ab0df6f4b7362786916bd89f353d0739fffa534368d8d793816", + "0x710108dddc39e579dcf0819f9ad107b3c56d1713530dd94325db1d853a675a37", + "0x8862b5f428ce5da50c89afb50aa779bb2c4dfe60e6f6a070b3a0208a4a970fe5", + "0x54a9cd342fa3a4bf685c01d1ce84f3068b0d5b6a58ee22dda8fbac4908bb9560", + "0x0fa3800efeaddd28247e114a1cf0f86b9014ccae9c3ee5f8488168b1103c1b44", + "0xbb393428b7ebfe2eda218730f93925d2e80c020d41a29f4746dcbb9138f7233a", + "0x7b42710942ef38ef2ff8fe44848335f26189c88c22a49fda84a51512ac68cd5d", + "0x90e99786a3e8b04db95ccd44d01e75558d75f3ddd12a1e9a2c2ce76258bf4813", + "0x3f6f71e40251728aa760763d25deeae54dc3a9b53807c737deee219120a2230a", + "0xe56081a7933c6eaf4ef2c5a04e21ab8a3897785dd83a34719d1b62d82cfd00c2", + "0x76cc54fa15f53e326575a9a2ac0b8ed2869403b6b6488ce4f3934f17db0f6bee", + "0x1cd9cd1d882ea3830e95162b5de4beb5ddff34fdbf7aec64e83b82a6d11b417c", + "0xb8ca8ae36d717c448aa27405037e44d9ee28bb8c6cc538a5d22e4535c8befd84", + "0x5c4492108c25f873a23d5fd7957b3229edc22858e8894febe7428c0831601982", + "0x907bcd75e7465e9791dc34e684742a2c0dc7007736313a95070a7e6b961c9c46", + "0xe7134b1511559e6b2440672073fa303ec3915398e75086149eb004f55e893214", + "0x2ddc2415e4753bfc383d48733e8b2a3f082883595edc5515514ebb872119af09", + "0xf2ad0f76b08ffa1eee62228ba76f4982fab4fbede5d4752c282c3541900bcd5b", + "0x0a84a6b15abd1cbc2da7092bf7bac418b8002b7000236dfba7c8335f27e0f1d4", + "0x97404e02b9ff5478c928e1e211850c08cc553ebac5d4754d13efd92588b1f20d", + "0xfa6ca3bcff1f45b557cdec34cb465ab06ade397e9d9470a658901e1f0f124659", + "0x5bd972d55f5472e5b08988ee4bccc7240a8019a5ba338405528cc8a38b29bc21", + "0x52952e4f96c803bb76749800891e3bfe55f7372facd5b5a587a39ac10b161bcc", + "0xf96731ae09abcad016fd81dc4218bbb5b2cb5fe2e177a715113f381814007314", + "0xe7d79e07cf9f2b52623491519a21a0a3d045401a5e7e10dd8873a85076616326", + "0xe4892f3777a4614ee6770b22098eaa0a3f32c5c44b54ecedacd69789d676dffe", + "0x20c932574779e2cc57780933d1dc6ce51a5ef920ce5bf681f7647ac751106367", + "0x057252c573908e227cc07797117701623a4835f4b047dcaa9678105299e48e70", + "0x20bad780930fa2a036fe1dea4ccbf46ac5b3c489818cdb0f97ae49d6e2f11fbf", + "0xc0d7dd26ffecdb098585a1694e45a54029bb1e31c7c5209289058efebb4cc91b", + "0x9a8744beb1935c0abe4b11812fc02748ef7c8cb650db3024dde3c5463e9d8714", + "0x8ce6eea4585bbeb657b326daa4f01f6aef34954338b3ca42074aedd1110ba495", + "0x1c85b43f5488b370721290d2faea19d9918d094c99963d6863acdfeeca564363", + "0xe88a244347e448349e32d0525b40b18533ea227a9d3e9b78a9ff14ce0a586061", + "0x352ca61efc5b8ff9ee78e738e749142dd1606154801a1449bbb278fa6bcc3dbe", + "0xa066926f9209220b24ea586fb20eb8199a05a247c82d7af60b380f6237429be7", + "0x3052337ccc990bfbae26d2f9fe5d7a4eb8edfb83a03203dca406fba9f4509b6e", + "0x343ce573a93c272688a068d758df53c0161aa7f9b55dec8beced363a38b33069", + "0x0f16b5593f133b58d706fe1793113a10750e8111eadee65301df7a1e84f782d3", + "0x808ae8539357e85b648020f1e9d255bc4114bee731a6220d7c5bcb5b85224e03", + "0x3b2bd97e31909251752ac57eda6015bb05b85f2838d475095cfd146677430625", + "0xe4f857c93b2d8b250050c7381a6c7c660bd29066195806c8ef11a2e6a6640236", + "0x23d91589b5070f443ddcefa0838c596518d54928119251ecf3ec0946a8128f52", + "0xb72736dfad52503c7f5f0c59827fb6ef4ef75909ff9526268abc0f296ee37296", + "0x80a8c66436d86b8afe87dde7e53a53ef87e057a5d4995963e76d159286de61b6", + "0xbec92c09ee5e0c84d5a8ba6ca329683ff550ace34631ea607a3a21f99cd36d67", + "0x83c97c9807b9ba6d9d914ae49dabdb4c55e12e35013f9b179e6bc92d5d62222b", + "0x8d9c79f6af3920672dc4cf97a297c186e75083d099aeb5c1051207bad0c98964", + "0x2aaa5944a2bd852b0b1be3166e88f357db097b001c1a71ba92040b473b30a607", + "0x46693d27ec4b764fbb516017c037c441f4558aebfe972cdcd03da67c98404e19", + "0x903b25d9e12208438f203c9ae2615b87f41633d5ffda9cf3f124c1c3922ba08f", + "0x3ec23dc8bc1b49f5c7160d78008f3f235252086a0a0fa3a7a5a3a53ad29ec410", + "0xa1fe74ceaf3cccd992001583a0783d7d7b7a245ea374f369133585b576b9c6d8", + "0xb2d6b0fe4932a2e06b99531232398f39a45b0f64c3d4ebeaaebc8f8e50a80607", + "0xe19893353f9214eebf08e5d83c6d44c24bffe0eceee4dc2e840d42eab0642536", + "0x5b798e4bc099fa2e2b4b5b90335c51befc9bbab31b4dd02451b0abd09c06ee79", + "0xbab2cdec1553a408cac8e61d9e6e19fb8ccfb48efe6d02bd49467a26eeeca920", + "0x1c1a544c28c38e5c423fe701506693511b3bc5f2af9771b9b2243cd8d41bebfc", + "0x704d6549d99be8cdefeec9a58957f75a2be4af7bc3dc4655fa606e7f3e03b030", + "0x051330f43fe39b08ed7d82d68c49b36a8bfa31357b546bfb32068712df89d190", + "0xe69174c7b03896461cab2dfaab33d549e3aac15e6b0f6f6f466fb31dae709b9b", + "0xe5f668603e0ddbbcde585ac41c54c3c4a681fffb7a5deb205344de294758e6ac", + "0xca70d5e4c3a81c1f21f246a3f52c41eaef9a683f38eb7c512eac8b385f46cbcd", + "0x3173a6b882b21cd147f0fc60ef8f24bbc42104caed4f9b154f2d2eafc3a56907", + "0xc71469c192bf5cc36242f6365727f57a19f924618b8a908ef885d8f459833cc3", + "0x59c596fc388afd8508bd0f5a1e767f3dda9ed30f6646d15bc59f0b07c4de646f", + "0xb200faf29368581f551bd351d357b6fa8cbf90bdc73b37335e51cad36b4cba83", + "0x275cede69b67a9ee0fff1a762345261cb20fa8191470159cc65c7885cfb8313c", + "0x0ce4ef84916efbe1ba9a0589bed098793b1ea529758ea089fd79151cc9dc7494", + "0x0f08483bb720e766d60a3cbd902ce7c9d835d3f7fdf6dbe1f37bcf2f0d4764a2", + "0xb30a73e5db2464e6da47d10667c82926fa91fceb337d89a52db5169008bc6726", + "0x6b9c50fed1cc404bf2dd6fffbfd18e30a4caa1500bfeb080aa93f78d10331aaf", + "0xf17c84286df03ce175966f560600dd562e0f59f18f1d1276b4d8aca545d57856", + "0x11455f2ef96a6b2be69854431ee219806008eb80ea38c81e45b2e58b3f975a20", + "0x9a61e03e2157a5c403dfcde690f7b7d704dd56ea1716cf14cf7111075a8d6491", + "0x30312c910ce6b39e00dbaa669f0fb7823a51f20e83eaeb5afa63fb57668cc2f4", + "0x17c18d261d94fba82886853a4f262b9c8b915ed3263b0052ece5826fd7e7d906", + "0x2d8f6ea0f5b9d0e4bc1478161f5ed2ad3d8495938b414dcaec9548adbe572671", + "0x19954625f13d9bab758074bf6dee47484260d29ee118347c1701aaa74abd9848", + "0x842ef2ad456e6f53d75e91e8744b96398df80350cf7af90b145fea51fbbcf067", + "0x34a8b0a76ac20308aa5175710fb3e75c275b1ff25dba17c04e3a3e3c48ca222c", + "0x58efcbe75f32577afe5e9ff827624368b1559c32fcca0cf4fd704af8ce019c63", + "0x411b4d242ef8f14d92bd8b0b01cb4fa3ca6f29c6f9073cfdd3ce614fa717463b", + "0xf76dbda66ede5e789314a88cff87ecb4bd9ca418c75417d4d920e0d21a523257", + "0xd801821a0f87b4520c1b003fe4936b6852c410ee00b46fb0f81621c9ac6bf6b4", + "0x97ad11d6a29c8cf3c548c094c92f077014de3629d1e9053a25dbfaf7eb55f72d", + "0xa87012090cd19886d49521d564ab2ad0f18fd489599050c42213bb960c9ee8ff", + "0x8868d8a26e758d50913f2bf228da0444a206e52853bb42dd8f90f09abe9c859a", + "0xc257fb0cc9970e02830571bf062a14540556abad2a1a158f17a18f14b8bcbe95", + "0xfe611ce27238541b14dc174b652dd06719dfbcda846a027f9d1a9e8e9df2c065", + "0xc9b25ea410f420cc2d4fc6057801d180c6cab959bce56bf6120f555966e6de6d", + "0x95437f0524ec3c04d4132c83be7f1a603e6f4743a85ede25aa97a1a4e3f3f8fc", + "0x82a12910104065f35e983699c4b9187aed0ab0ec6146f91728901efecc7e2e20", + "0x6622dd11e09252004fb5aaa39e283333c0686065f228c48a5b55ee2060dbd139", + "0x89a2879f25733dab254e4fa6fddb4f04b8ddf018bf9ad5c162aea5c858e6faaa", + "0x8a71b62075a6011fd9b65d956108fa79cc9ebb8f194d64d3105a164e01cf43a6", + "0x103f4fe9ce211b6452181371f0dc4a30a557064b684645a4495136f4ebd0936a", + "0x97914adc5d7ce80147c2f44a6b29d0b495d38dedd8cc299064abcc62ed1ddabc", + "0x825c481da6c836a8696d7fda4b0563d204a9e7d9e4c47b46ded26db3e2d7d734", + "0xf8c0637ba4c0a383229f1d730db733bc11d6a4e33214216c23f69ec965dcaaad", + "0xaed3bdaf0cb12d37764d243ee0e8acdefc399be2cabbf1e51dc43454efd79cbd", + "0xe8427f56cc5cec8554e2f5f586b57adccbea97d5fc3ef7b8bbe97c2097cf848c", + "0xba4ad0abd5c14d526357fd0b6f8676ef6126aeb4a6d80cabe1f1281b9d28246c", + "0x4cff20b72e2ab5af3fafbf9222146949527c25f485ec032f22d94567ff91b22f", + "0x0d32925d89dd8fed989912afcbe830a4b5f8f7ae1a3e08ff1d3a575a77071d99", + "0xe51a1cbeae0be5d2fdbc7941aea904d3eade273f7477f60d5dd6a12807246030", + "0xfb8615046c969ef0fa5e6dc9628c8a9880e86a5dc2f6fc87aff216ea83fcf161", + "0x64dd705e105c88861470d112c64ca3d038f67660a02d3050ea36c34a9ebf47f9", + "0xb6ad148095c97528180f60fa7e8609bf5ce92bd562682092d79228c2e6f0750c", + "0x5bae0cd81f3bd0384ca3143a72068e6010b946462a73299e746ca639c026781c", + "0xc39a0fc7764fcfc0402b12fb0bbe78fe3633cbfb33c7f849279585a878a26d7c", + "0x2b752fda1c0c53d685cc91144f78d371db6b766725872b62cc99e1234cca8c1a", + "0x40ee6b9635d87c95a528757729212a261843ecb06d975de91352d43ca3c7f196", + "0x75e2005d3726cf8a4bb97ea5287849a361e3f8fdfadc3c1372feed1208c89f6b", + "0x0976f8ab556153964b58158678a5297da4d6ad92e284da46052a791ee667aee4", + "0xdbeef07841e41e0672771fb550a5b9233ae8e9256e23fa0d34d5ae5efe067ec8", + "0xa890f412ab6061c0c5ee661e80d4edc5c36b22fb79ac172ddd5ff26a7dbe9751", + "0xb666ae07f9276f6d0a33f9efeb3c5cfcba314fbc06e947563db92a40d7a341e8", + "0x83a082cf97ee78fbd7f31a01ae72e40c2e980a6dab756161544c27da86043528", + "0xfa726a919c6f8840c456dc77b0fec5adbed729e0efbb9317b75f77ed479c0f44", + "0xa8606800c54faeab2cbc9d85ff556c49dd7e1a0476027e0f7ce2c1dc2ba7ccbf", + "0x2796277836ab4c17a584c9f6c7778d10912cb19e541fb75453796841e1f6cd1c", + "0xf648b8b3c7be06f1f8d9cda13fd6d60f913e5048a8e0b283b110ca427eeb715f", + "0xa21d00b8fdcd77295d4064e00fbc30bed579d8255e9cf3a9016911d832390717", + "0xe741afcd98cbb3bb140737ed77bb968ac60d5c00022d722f9f04f56e97235dc9", + "0xbeecc9638fac39708ec16910e5b02c91f83f6321f6eb658cf8a96353cfb49806", + "0x912eee6cabeb0fed8d6e6ca0ba61977fd8e09ea0780ff8fbec995e2a85e08b52", + "0xc665bc0bb121a1229bc56ecc07a7e234fd24c523ea14700aa09e569b5f53ad33", + "0x39501621c2bdff2f62ab8d8e3fe47fe1701a98c665697c5b750ee1892f11846e", + "0x03d32e16c3a6c913daefb139f131e1e95a742b7be8e20ee39b785b4772a50e44", + "0x4f504eb46a82d440f1c952a06f143994bc66eb9e3ed865080cd9dfc6d652b69c", + "0xad753dc8710a46a70e19189d8fc7f4c773e4d9ccc7a70c354b574fe377328741", + "0xf7f5464a2d723b81502adb9133a0a4f0589b4134ca595a82e660987c6b011610", + "0x216b60b1c3e3bb4213ab5d43e04619d13e1ecedbdd65a1752bda326223e3ca3e", + "0x763664aa96d27b6e2ac7974e3ca9c9d2a702911bc5d550d246631965cf2bd4a2", + "0x292b5c8c8431b040c04d631f313d4e6b67b5fd3d4b8ac9f2edb09d13ec61f088", + "0x80db43c2b9e56eb540592f15f5900222faf3f75ce62e78189b5aa98c54568a5e", + "0x1b5fdf8969bcd4d65e86a2cefb3a673e18d587843f4f50db4e3ee77a0ba2ef1c", + "0x11e237953fff3e95e6572da50a92768467ffdfd0640d3384aa1c486357e7c24a", + "0x1fabd4faa8dba44808cc87d0bc389654a98496745578f3d17d134adc7f7b10f3", + "0x5eca4aa96f20a56197772ae6b600762154ca9d2702cab12664ea47cbff1a440c", + "0x0b4234f5bb02abcf3b5ce6c44ea85f55ec7db98fa5a7b90abef6dd0df034743c", + "0x316761e295bf350313c4c92efea591b522f1df4211ce94b22e601f30aefa51ef", + "0xe93a55ddb4d7dfe02598e8f909ff34b3de40a1c0ac8c7fba48cb604ea60631fb", + "0xe6e6c877b996857637f8a71d0cd9a6d47fdeb03752c8965766f010073332b087", + "0xa4f95c8874e611eddd2c4502e4e1196f0f1be90bfc37db35f8588e7d81d34aeb", + "0x9351710a5633714bb8b2d226e15ba4caa6f50f56c5508e5fa1239d5cc6a7e1aa", + "0x8d0aef52ec7266f37adb572913a6213b8448caaf0384008373dec525ae6cdff1", + "0x718e24c3970c85bcb14d2763201812c43abac0a7f16fc5787a7a7b2f37288586", + "0x3600ce44cebc3ee46b39734532128eaf715c0f3596b554f8478b961b0d6e389a", + "0x50dd1db7b0a5f6bd2d16252f43254d0f5d009e59f61ebc817c4bbf388519a46b", + "0x67861ed00f5fef446e1f4e671950ac2ddae1f3b564f1a6fe945e91678724ef03", + "0x0e332c26e169648bc20b4f430fbf8c26c6edf1a235f978d09d4a74c7b8754aad", + "0x6c9901015adf56e564dfb51d41a82bde43fb67273b6911c9ef7fa817555c9557", + "0x53c83391e5e0a024f68d5ade39b7a769f10664e12e4942c236398dd5dbce47a1", + "0x78619564f0b2399a9fcb229d938bf1e298d62b03b7a37fe6486034185d7f7d27", + "0x4625f15381a8723452ec80f3dd0293c213ae35de737c508f42427e1735398c3a", + "0x69542425ddb39d3d3981e76b41173eb1a09500f11164658a3536bf3e292f8b6a", + "0x82ac4f5bb40aece7d6706f1bdf4dfba5c835c09afba6446ef408d8ec6c09300f", + "0x740f9180671091b4c5b3ca59b9515bd0fc751f48e488a9f7f4b6848602490e21", + "0x9a04b08b4115986d8848e80960ad67490923154617cb82b3d88656ec1176c24c", + "0xf9ffe528eccffad519819d9eef70cef317af33899bcaee16f1e720caf9a98744", + "0x46da5e1a14b582b237f75556a0fd108c4ea0d55c0edd8f5d06c59a42e57410df", + "0x098f3429c8ccda60c3b5b9755e5632dd6a3f5297ee819bec8de2d8d37893968a", + "0x1a5b91af6025c11911ac072a98b8a44ed81f1f3c76ae752bd28004915db6f554", + "0x8bed50c7cae549ed4f8e05e02aa09b2a614c0af8eec719e4c6f7aee975ec3ec7", + "0xd86130f624b5dcc116f2dfbb5219b1afde4b7780780decd0b42694e15c1f8d8b", + "0x4167aa9bc0075f624d25d40eb29139dd2c452ebf17739fab859e14ac6765337a", + "0xa258ce5db20e91fb2ea30d607ac2f588bdc1924b21bbe39dc881e19889a7f5c6", + "0xe5ef8b5ab3cc8894452d16dc875b69a55fd925808ac7cafef1cd19485d0bb50a", + "0x120df2b3975d85b6dfca56bb98a82025ade5ac1d33e4319d2e0105b8de9ebf58", + "0xc964291dd2e0807a468396ebba3d59cfe385d949f6d6215976fc9a0a11de209a", + "0xf23f14cb709074b79abe166f159bc52b50de687464df6a5ebf112aa953c95ad5", + "0x622c092c9bd7e30f880043762e26d8e9c73ab7c0d0806f3c5e472a4152b35a93", + "0x8a5f090662731e7422bf651187fb89812419ab6808f2c62da213d6944fccfe9f", + "0xfbea3c0d92e061fd2399606f42647d65cc54191fa46d57b325103a75f5c22ba6", + "0x2babfbcc08d69b52c3747ddc8dcad4ea5511edabf24496f3ff96a1194d6f680e", + "0x4d3d019c28c779496b616d85aee201a3d79d9eecf35f728d00bcb12245ace703", + "0xe76fcee1f08325110436f8d4a95476251326b4827399f9b2ef7e12b7fb9c4ba1", + "0x4884d9c0bb4a9454ea37926591fc3eed2a28356e0506106a18f093035638da93", + "0x74c3f303d93d4cc4f0c1eb1b4378d34139220eb836628b82b649d1deb519b1d3", + "0xacb806670b278d3f0c84ba9c7a68c7df3b89e3451731a55d7351468c7c864c1c", + "0x8660fb8cd97e585ea7a41bccb22dd46e07eee8bbf34d90f0f0ca854b93b1ebee", + "0x2fc9c89cdca71a1c0224d469d0c364c96bbd99c1067a7ebe8ef412c645357a76", + "0x8ec6d5ab6ad7135d66091b8bf269be44c20af1d828694cd8650b5479156fd700", + "0x50ab4776e8cabe3d864fb7a1637de83f8fbb45d6e49645555ffe9526b27ebd66", + "0xbf39f5e17082983da4f409f91c7d9059acd02ccbefa69694aca475bb8d40b224", + "0x3135b3b981c850cc3fe9754ec6af117459d355ad6b0915beb61e84ea735c31bf", + "0xa7971dab52ce4bf45813223b0695f8e87f64b614c9c5499faac6f842e5c41be9", + "0x9e480f5617323ab104b4087ac4ef849a5da03427712fb302ac085507c77d8f37", + "0x57a6d474654d5e8d408159be39ad0e7026e6a4c6a6543e23a63d30610dc8dfc1", + "0x09eb3e01a5915a4e26d90b4c58bf0cf1e560fdc8ba53faed9d946ad3e9bc78fa", + "0x29c6d25da80a772310226b1b89d845c7916e4a4bc94d75aa330ec3eaa14b1e28", + "0x1a1ccfee11edeb989ca02e3cb89f062612a22a69ec816a625835d79370173987", + "0x1cb63dc541cf7f71c1c4e8cabd2619c3503c0ea1362dec75eccdf1e9efdbfcfc", + "0xac9dff32a69e75b396a2c250e206b36c34c63b955c9e5732e65eaf7ccca03c62", + "0x3e1b4f0c3ebd3d38cec389720147746774fc01ff6bdd065f0baf2906b16766a8", + "0x5cc8bed25574463026205e90aad828521f8e3d440970d7e810d1b46849681db5", + "0x255185d264509bd3a768bb0d50b568e66eb1fec96d573e33aaacc716d7c8fb93", + "0xe81b86ba631973918a859ff5995d7840b12511184c2865401f2693a71b9fa07e", + "0x61e67e42616598da8d36e865b282127c761380d3a56d26b8d35fbbc7641433c5", + "0x60c62ffef83fe603a34ca20b549522394e650dad5510ae68b6e074f0cd209a56", + "0x78577f2caf4a54f6065593535d76216f5f4075af7e7a98b79571d33b1822920c", + "0xfd4cb354f2869c8650200de0fe06f3d39e4dbebf19b0c1c2677da916ea84f44d", + "0x453769cef6ff9ba2d5c917982a1ad3e2f7e947d9ea228857556af0005665e0b0", + "0xe567f93f8f88bf1a6b33214f17f5d60c5dbbb531b4ab21b8c0b799b6416891e0", + "0x7e65a39a17f902a30ceb2469fe21cba8d4e0da9740fcefd5c647c81ff1ae95fa", + "0x03e4a7eea0cd6fc02b987138ef88e8795b5f839636ca07f6665bbae9e5878931", + "0xc3558e2b437cf0347cabc63c95fa2710d3f43c65d380feb998511903f9f4dcf0", + "0xe3a615f80882fb5dfbd08c1d7a8b0a4d3b651d5e8221f99b879cb01d97037a9c", + "0xb56db4a5fea85cbffaee41f05304689ea321c40d4c108b1146fa69118431d9b2", + "0xab28e1f077f18117945910c235bc9c6f9b6d2b45e9ef03009053006c637e3e26", + "0xefcabc1d5659fd6e48430dbfcc9fb4e08e8a9b895f7bf9b3d6c7661bfc44ada2", + "0xc7547496f212873e7c3631dafaca62a6e95ac39272acf25a7394bac6ea1ae357", + "0xc482013cb01bd69e0ea9f447b611b06623352e321469f4adc739e3ee189298eb", + "0x5942f42e91e391bb44bb2c4d40da1906164dbb6d1c184f00fa62899baa0dba2c", + "0xb4bcb46c80ad4cd603aff2c1baf8f2c896a628a46cc5786f0e58dae846694677", + "0xd0a7305b995fa8c317c330118fee4bfef9f65f70b54558c0988945b08e90ff08", + "0x687f801b7f32fdfa7d50274cc7b126efedbdae8de154d36395d33967216f3086", + "0xeb19ec10ac6c15ffa619fa46792971ee22a9328fa53bd69a10ed6e9617dd1bbf", + "0xa2bb3f0367f62abdb3a9fa6da34b20697cf214a4ff14fd42826da140ee025213", + "0x070a76511f32c882374400af59b22d88974a06fbc10d786dd07ca7527ebd8b90", + "0x8f195689537b446e946b376ec1e9eb5af5b4542ab47be550a5700fa5d81440d5", + "0x10cc09778699fc8ac109e7e6773f83391eeba2a6db5226fbe953dd8d99126ca5", + "0x8cc839cb7dc84fd3b8c0c7ca637e86a2f72a8715cc16c7afb597d12da717530b", + "0xa32504e6cc6fd0ee441440f213f082fcf76f72d36b5e2a0f3b6bdd50cdd825a2", + "0x8f45151db8878e51eec12c450b69fa92176af21a4543bb78c0d4c27286e74469", + "0x23f5c465bd35bcd4353216dc9505df68324a27990df9825a242e1288e40a13bb", + "0x35f409ce748af33c20a6ae693b8a48ba4623de9686f9834e22be4410e637d24f", + "0xb962e5845c1db624532562597a99e2acc5e434b97d8db0725bdeddd71a98e737", + "0x0f8364f99f43dd52b4cfa9e426c48f7b6ab18dc40a896e96a09eceebb3363afe", + "0xa842746868da7644fccdbb07ae5e08c71a6287ab307c4f9717eadb414c9c99f4", + "0xa59064c6b7fe7d2407792d99ed1218d2dc2f240185fbd8f767997438241b92e9", + "0xb6ea0d58e8d48e05b9ff4d75b2ebe0bd9752c0e2691882f754be66cdec7628d3", + "0xf16b78c9d14c52b2b5156690b6ce37a5e09661f49674ad22604c7d3755e564d1", + "0xbfa8ef74e8a37cd64b8b4a4260c4fc162140603f9c2494b9cf4c1e13de522ed9", + "0xf4b89f1776ebf30640dc5ec99e43de22136b6ef936a85193ef940931108e408a", + "0xefb9a4555d495a584dbcc2a50938f6b9827eb014ffae2d2d0aae356a57894de8", + "0x0627a466d42a26aca72cf531d4722e0e5fc5d491f4527786be4e1b641e693ac2", + "0x7d10d21542de3d8f074dbfd1a6e11b3df32c36272891aae54053029d39ebae10", + "0x0f21118ee9763f46cc175a21de876da233b2b3b62c6f06fa2df73f6deccf37f3", + "0x143213b96f8519c15164742e2350cc66e814c9570634e871a8c1ddae4d31b6b5", + "0x8d2877120abae3854e00ae8cf5c8c95b3ede10590ab79ce2be7127239507e18d", + "0xaccd0005d59472ac04192c059ed9c10aea42c4dabec9e581f6cb10b261746573", + "0x67bc8dd5422f39e741b9995e6e60686e75d6620aa0d745b84191f5dba9b5bb18", + "0x11b8e95f6a654d4373cefbbac29a90fdd8ae098043d1969b9fa7885318376b34", + "0x431a0b8a6f08760c942eeff5791e7088fd210f877825ce4dcabe365e03e4a65c", + "0x704007f11bae513f428c9b0d23593fd2809d0dbc4c331009856135dafec23ce4", + "0xc06dee39a33a05e30c522061c1d9272381bde3f9e42fa9bd7d5a5c8ef11ec6ec", + "0x66b4157baaae85db0948ad72882287a80b286df2c40080b8da4d5d3db0a61bd2", + "0xef1983b1906239b490baaaa8e4527f78a57a0a767d731f062dd09efb59ae8e3d", + "0xf26d0d5c520cce6688ca5d51dee285af26f150794f2ea9f1d73f6df213d78338", + "0x8b28838382e6892f59c42a7709d6d38396495d3af5a8d5b0a60f172a6a8940bd", + "0x261a605fa5f2a9bdc7cffac530edcf976e7ea7af4e443b625fe01ed39dad44b6", + ], + compressed_lamport_pk: + "0xdd635d27d1d52b9a49df9e5c0c622360a4dd17cba7db4e89bce3cb048fb721a5", + child_sk: + "20397789859736650942317412262472558107875392172444076792671091975210932703118", } } } diff --git a/crypto/eth2_key_derivation/tests/eip2333_vectors.rs b/crypto/eth2_key_derivation/tests/eip2333_vectors.rs index 6995bd087..e4406ab1f 100644 --- a/crypto/eth2_key_derivation/tests/eip2333_vectors.rs +++ b/crypto/eth2_key_derivation/tests/eip2333_vectors.rs @@ -65,9 +65,9 @@ fn assert_vector_passes(raw: RawTestVector) { fn eip2333_test_case_0() { assert_vector_passes(RawTestVector { seed: "0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", - master_sk: "12513733877922233913083619867448865075222526338446857121953625441395088009793", + master_sk: "6083874454709270928345386274498605044986640685124978867557563392430687146096", child_index: 0, - child_sk: "7419543105316279183937430842449358701327973165530407166294956473095303972104" + child_sk: "20397789859736650942317412262472558107875392172444076792671091975210932703118", }) } @@ -75,9 +75,9 @@ fn eip2333_test_case_0() { fn eip2333_test_case_1() { assert_vector_passes(RawTestVector { seed: "0x3141592653589793238462643383279502884197169399375105820974944592", - master_sk: "46029459550803682895343812821003080589696405386150182061394330539196052371668", + master_sk: "29757020647961307431480504535336562678282505419141012933316116377660817309383", child_index: 3141592653, - child_sk: "43469287647733616183478983885105537266268532274998688773496918571876759327260", + child_sk: "25457201688850691947727629385191704516744796114925897962676248250929345014287", }) } @@ -85,9 +85,9 @@ fn eip2333_test_case_1() { fn eip2333_test_case_2() { assert_vector_passes(RawTestVector { seed: "0x0099FF991111002299DD7744EE3355BBDD8844115566CC55663355668888CC00", - master_sk: "45379166311535261329029945990467475187325618028073620882733843918126031931161", + master_sk: "27580842291869792442942448775674722299803720648445448686099262467207037398656", child_index: 4294967295, - child_sk: "46475244006136701976831062271444482037125148379128114617927607151318277762946", + child_sk: "29358610794459428860402234341874281240803786294062035874021252734817515685787", }) } @@ -95,8 +95,8 @@ fn eip2333_test_case_2() { fn eip2333_test_case_3() { assert_vector_passes(RawTestVector { seed: "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", - master_sk: "31740500954810567003972734830331791822878290325762596213711963944729383643688", + master_sk: "19022158461524446591288038168518313374041767046816487870552872741050760015818", child_index: 42, - child_sk: "51041472511529980987749393477251359993058329222191894694692317000136653813011", + child_sk: "31372231650479070279774297061823572166496564838472787488249775572789064611981", }) } From dffc56ef1d79359a5751189c13e3b509c407f866 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 24 Sep 2020 04:06:02 +0000 Subject: [PATCH 02/32] Fix validator lockfiles (#1586) ## Issue Addressed - Resolves #1313 ## Proposed Changes Changes the way we start the validator client and beacon node to ensure that we cleanly drop the validator keystores (which therefore ensures we cleanup their lockfiles). Previously we were holding the validator keystores in a tokio task that was being forcefully killed (i.e., without `Drop`). Now, we hold them in a task that can gracefully handle a shutdown. Also, switches the `--strict-lockfiles` flag to `--delete-lockfiles`. This means two things: 1. We are now strict on lockfiles by default (before we weren't). 1. There's a simple way for people delete the lockfiles if they experience a crash. ## Additional Info I've only given the option to ignore *and* delete lockfiles, not just ignore them. I can't see a strong need for ignore-only but could easily add it, if the need arises. I've flagged this as `api-breaking` since users that have lockfiles lingering around will be required to supply `--delete-lockfiles` next time they run. --- beacon_node/src/lib.rs | 5 +- lighthouse/src/main.rs | 96 ++++++------- validator_client/src/cli.rs | 12 +- validator_client/src/config.rs | 8 +- .../src/initialized_validators.rs | 51 ++++--- validator_client/src/lib.rs | 126 ++++++++++-------- 6 files changed, 168 insertions(+), 130 deletions(-) diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 199319160..a09f8c6cd 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -7,7 +7,7 @@ mod config; pub use beacon_chain; pub use cli::cli_app; pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis}; -pub use config::{get_data_dir, get_eth2_testnet_config, set_network_config}; +pub use config::{get_config, get_data_dir, get_eth2_testnet_config, set_network_config}; pub use eth2_config::Eth2Config; use beacon_chain::events::TeeEventHandler; @@ -17,7 +17,6 @@ use beacon_chain::{ builder::Witness, eth1_chain::CachingEth1Backend, slot_clock::SystemTimeSlotClock, }; use clap::ArgMatches; -use config::get_config; use environment::RuntimeContext; use slog::{info, warn}; use std::ops::{Deref, DerefMut}; @@ -54,7 +53,7 @@ impl ProductionBeaconNode { /// configurations hosted remotely. pub async fn new_from_cli( context: RuntimeContext, - matches: &ArgMatches<'_>, + matches: ArgMatches<'static>, ) -> Result { let client_config = get_config::( &matches, diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 2df5c3539..c174992e0 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -255,61 +255,63 @@ fn run( "name" => testnet_name ); - let beacon_node = if let Some(sub_matches) = matches.subcommand_matches("beacon_node") { - let runtime_context = environment.core_context(); + match matches.subcommand() { + ("beacon_node", Some(matches)) => { + let context = environment.core_context(); + let log = context.log().clone(); + let executor = context.executor.clone(); + let config = beacon_node::get_config::( + matches, + &context.eth2_config.spec_constants, + &context.eth2_config().spec, + context.log().clone(), + )?; + environment.runtime().spawn(async move { + if let Err(e) = ProductionBeaconNode::new(context.clone(), config).await { + crit!(log, "Failed to start beacon node"; "reason" => e); + // Ignore the error since it always occurs during normal operation when + // shutting down. + let _ = executor + .shutdown_sender() + .try_send("Failed to start beacon node"); + } + }) + } + ("validator_client", Some(matches)) => { + let context = environment.core_context(); + let log = context.log().clone(); + let executor = context.executor.clone(); + let config = validator_client::Config::from_cli(&matches) + .map_err(|e| format!("Unable to initialize validator config: {}", e))?; + environment.runtime().spawn(async move { + let run = async { + ProductionValidatorClient::new(context, config) + .await? + .start_service()?; - let beacon = environment - .runtime() - .block_on(ProductionBeaconNode::new_from_cli( - runtime_context, - sub_matches, - )) - .map_err(|e| format!("Failed to start beacon node: {}", e))?; - - Some(beacon) - } else { - None + Ok::<(), String>(()) + }; + if let Err(e) = run.await { + crit!(log, "Failed to start validator client"; "reason" => e); + // Ignore the error since it always occurs during normal operation when + // shutting down. + let _ = executor + .shutdown_sender() + .try_send("Failed to start validator client"); + } + }) + } + _ => { + crit!(log, "No subcommand supplied. See --help ."); + return Err("No subcommand supplied.".into()); + } }; - let validator_client = if let Some(sub_matches) = matches.subcommand_matches("validator_client") - { - let runtime_context = environment.core_context(); - - let mut validator = environment - .runtime() - .block_on(ProductionValidatorClient::new_from_cli( - runtime_context, - sub_matches, - )) - .map_err(|e| format!("Failed to init validator client: {}", e))?; - - environment - .core_context() - .executor - .runtime_handle() - .enter(|| { - validator - .start_service() - .map_err(|e| format!("Failed to start validator client service: {}", e)) - })?; - - Some(validator) - } else { - None - }; - - if beacon_node.is_none() && validator_client.is_none() { - crit!(log, "No subcommand supplied. See --help ."); - return Err("No subcommand supplied.".into()); - } - // Block this thread until we get a ctrl-c or a task sends a shutdown signal. environment.block_until_shutdown_requested()?; info!(log, "Shutting down.."); environment.fire_signal(); - drop(beacon_node); - drop(validator_client); // Shutdown the environment once all tasks have completed. environment.shutdown_on_idle(); diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index ed320c24c..7ac483439 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -37,11 +37,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { nodes using the same key. Automatically enabled unless `--strict` is specified", )) .arg( - Arg::with_name("strict-lockfiles") - .long("strict-lockfiles") + Arg::with_name("delete-lockfiles") + .long("delete-lockfiles") .help( - "If present, do not load validators that are guarded by a lockfile. Note: for \ - Eth2 mainnet, this flag will likely be removed and its behaviour will become default." + "If present, ignore and delete any keystore lockfiles encountered during start up. \ + This is useful if the validator client did not exit gracefully on the last run. \ + WARNING: lockfiles help prevent users from accidentally running the same validator \ + using two different validator clients, an action that likely leads to slashing. \ + Ensure you are certain that there are no other validator client instances running \ + that might also be using the same keystores." ) ) .arg( diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 482c4ed70..4a11c5aec 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -24,8 +24,8 @@ pub struct Config { /// If true, the validator client will still poll for duties and produce blocks even if the /// beacon node is not synced at startup. pub allow_unsynced_beacon_node: bool, - /// If true, refuse to unlock a keypair that is guarded by a lockfile. - pub strict_lockfiles: bool, + /// If true, delete any validator keystore lockfiles that would prevent starting. + pub delete_lockfiles: bool, /// If true, don't scan the validators dir for new keystores. pub disable_auto_discover: bool, /// Graffiti to be inserted everytime we create a block. @@ -46,7 +46,7 @@ impl Default for Config { secrets_dir, http_server: DEFAULT_HTTP_SERVER.to_string(), allow_unsynced_beacon_node: false, - strict_lockfiles: false, + delete_lockfiles: false, disable_auto_discover: false, graffiti: None, } @@ -77,7 +77,7 @@ impl Config { } config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced"); - config.strict_lockfiles = cli_args.is_present("strict-lockfiles"); + config.delete_lockfiles = cli_args.is_present("delete-lockfiles"); config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); if let Some(secrets_dir) = parse_optional(cli_args, "secrets-dir")? { diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 436dcb4ba..400768f5c 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -54,6 +54,10 @@ pub enum Error { PasswordUnknown(PathBuf), /// There was an error reading from stdin. UnableToReadPasswordFromUser(String), + /// There was an error running a tokio async task. + TokioJoin(tokio::task::JoinError), + /// There was a filesystem error when deleting a lockfile. + UnableToDeleteLockfile(io::Error), } /// A method used by a validator to sign messages. @@ -86,7 +90,7 @@ impl InitializedValidator { /// If the validator is unable to be initialized for whatever reason. pub fn from_definition( def: ValidatorDefinition, - strict_lockfiles: bool, + delete_lockfiles: bool, log: &Logger, ) -> Result { if !def.enabled { @@ -150,16 +154,17 @@ impl InitializedValidator { })?; if voting_keystore_lockfile_path.exists() { - if strict_lockfiles { - return Err(Error::LockfileExists(voting_keystore_lockfile_path)); - } else { - // If **not** respecting lockfiles, just raise a warning if the voting - // keypair cannot be unlocked. + if delete_lockfiles { warn!( log, - "Ignoring validator lockfile"; + "Deleting validator lockfile"; "file" => format!("{:?}", voting_keystore_lockfile_path) ); + + fs::remove_file(&voting_keystore_lockfile_path) + .map_err(Error::UnableToDeleteLockfile)?; + } else { + return Err(Error::LockfileExists(voting_keystore_lockfile_path)); } } else { // Create a new lockfile. @@ -279,7 +284,7 @@ pub struct InitializedValidators { impl InitializedValidators { /// Instantiates `Self`, initializing all validators in `definitions`. - pub fn from_definitions( + pub async fn from_definitions( definitions: ValidatorDefinitions, validators_dir: PathBuf, strict_lockfiles: bool, @@ -292,7 +297,7 @@ impl InitializedValidators { validators: HashMap::default(), log, }; - this.update_validators()?; + this.update_validators().await?; Ok(this) } @@ -328,7 +333,7 @@ impl InitializedValidators { /// validator will be removed from `self.validators`. /// /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. - pub fn set_validator_status( + pub async fn set_validator_status( &mut self, voting_public_key: &PublicKey, enabled: bool, @@ -342,7 +347,7 @@ impl InitializedValidators { def.enabled = enabled; } - self.update_validators()?; + self.update_validators().await?; self.definitions .save(&self.validators_dir) @@ -362,7 +367,7 @@ impl InitializedValidators { /// A validator is considered "already known" and skipped if the public key is already known. /// I.e., if there are two different definitions with the same public key then the second will /// be ignored. - fn update_validators(&mut self) -> Result<(), Error> { + async fn update_validators(&mut self) -> Result<(), Error> { for def in self.definitions.as_slice() { if def.enabled { match &def.signing_definition { @@ -371,11 +376,23 @@ impl InitializedValidators { continue; } - match InitializedValidator::from_definition( - def.clone(), - self.strict_lockfiles, - &self.log, - ) { + // Decoding a local keystore can take several seconds, therefore it's best + // to keep if off the core executor. This also has the fortunate effect of + // interrupting the potentially long-running task during shut down. + let inner_def = def.clone(); + let strict_lockfiles = self.strict_lockfiles; + let inner_log = self.log.clone(); + let result = tokio::task::spawn_blocking(move || { + InitializedValidator::from_definition( + inner_def, + strict_lockfiles, + &inner_log, + ) + }) + .await + .map_err(Error::TokioJoin)?; + + match result { Ok(init) => { self.validators .insert(init.voting_public_key().clone(), init); diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 220d82a66..6b709023f 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -18,6 +18,7 @@ use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; use duties_service::{DutiesService, DutiesServiceBuilder}; use environment::RuntimeContext; +use eth2_config::Eth2Config; use fork_service::{ForkService, ForkServiceBuilder}; use futures::channel::mpsc; use initialized_validators::InitializedValidators; @@ -28,7 +29,7 @@ use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{delay_for, Duration}; -use types::EthSpec; +use types::{EthSpec, Hash256}; use validator_store::ValidatorStore; /// The interval between attempts to contact the beacon node during startup. @@ -90,9 +91,10 @@ impl ProductionValidatorClient { let validators = InitializedValidators::from_definitions( validator_defs, config.data_dir.clone(), - config.strict_lockfiles, + config.delete_lockfiles, log.clone(), ) + .await .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; info!( @@ -106,56 +108,11 @@ impl ProductionValidatorClient { RemoteBeaconNode::new_with_timeout(config.http_server.clone(), HTTP_TIMEOUT) .map_err(|e| format!("Unable to init beacon node http client: {}", e))?; - // TODO: check if all logs in wait_for_node are produed while awaiting - let beacon_node = wait_for_node(beacon_node, &log).await?; - let eth2_config = beacon_node - .http - .spec() - .get_eth2_config() - .await - .map_err(|e| format!("Unable to read eth2 config from beacon node: {:?}", e))?; - let genesis_time = beacon_node - .http - .beacon() - .get_genesis_time() - .await - .map_err(|e| format!("Unable to read genesis time from beacon node: {:?}", e))?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| format!("Unable to read system time: {:?}", e))?; - let genesis = Duration::from_secs(genesis_time); - - // If the time now is less than (prior to) genesis, then delay until the - // genesis instant. - // - // If the validator client starts before genesis, it will get errors from - // the slot clock. - if now < genesis { - info!( - log, - "Starting node prior to genesis"; - "seconds_to_wait" => (genesis - now).as_secs() - ); - - delay_for(genesis - now).await - } else { - info!( - log, - "Genesis has already occurred"; - "seconds_ago" => (now - genesis).as_secs() - ); - } - let genesis_validators_root = beacon_node - .http - .beacon() - .get_genesis_validators_root() - .await - .map_err(|e| { - format!( - "Unable to read genesis validators root from beacon node: {:?}", - e - ) - })?; + // Perform some potentially long-running initialization tasks. + let (eth2_config, genesis_time, genesis_validators_root) = tokio::select! { + tuple = init_from_beacon_node(&beacon_node, &context) => tuple?, + () = context.executor.exit() => return Err("Shutting down".to_string()) + }; // Do not permit a connection to a beacon node using different spec constants. if context.eth2_config.spec_constants != eth2_config.spec_constants { @@ -270,12 +227,71 @@ impl ProductionValidatorClient { } } +async fn init_from_beacon_node( + beacon_node: &RemoteBeaconNode, + context: &RuntimeContext, +) -> Result<(Eth2Config, u64, Hash256), String> { + // Wait for the beacon node to come online. + wait_for_node(beacon_node, context.log()).await?; + + let eth2_config = beacon_node + .http + .spec() + .get_eth2_config() + .await + .map_err(|e| format!("Unable to read eth2 config from beacon node: {:?}", e))?; + let genesis_time = beacon_node + .http + .beacon() + .get_genesis_time() + .await + .map_err(|e| format!("Unable to read genesis time from beacon node: {:?}", e))?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| format!("Unable to read system time: {:?}", e))?; + let genesis = Duration::from_secs(genesis_time); + + // If the time now is less than (prior to) genesis, then delay until the + // genesis instant. + // + // If the validator client starts before genesis, it will get errors from + // the slot clock. + if now < genesis { + info!( + context.log(), + "Starting node prior to genesis"; + "seconds_to_wait" => (genesis - now).as_secs() + ); + + delay_for(genesis - now).await; + } else { + info!( + context.log(), + "Genesis has already occurred"; + "seconds_ago" => (now - genesis).as_secs() + ); + } + let genesis_validators_root = beacon_node + .http + .beacon() + .get_genesis_validators_root() + .await + .map_err(|e| { + format!( + "Unable to read genesis validators root from beacon node: {:?}", + e + ) + })?; + + Ok((eth2_config, genesis_time, genesis_validators_root)) +} + /// Request the version from the node, looping back and trying again on failure. Exit once the node /// has been contacted. async fn wait_for_node( - beacon_node: RemoteBeaconNode, + beacon_node: &RemoteBeaconNode, log: &Logger, -) -> Result, String> { +) -> Result<(), String> { // Try to get the version string from the node, looping until success is returned. loop { let log = log.clone(); @@ -295,7 +311,7 @@ async fn wait_for_node( "version" => version, ); - return Ok(beacon_node); + return Ok(()); } Err(e) => { error!( From 8e201763376d56f487f11393fefa93d626d3742b Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 29 Sep 2020 00:02:44 +0000 Subject: [PATCH 03/32] Directory restructure (#1532) Closes #1487 Closes #1427 Directory restructure in accordance with #1487. Also has temporary migration code to move the old directories into new structure. Also extracts all default directory names and utility functions into a `directory` crate to avoid repetitio. ~Since `validator_definition.yaml` stores absolute paths, users will have to manually change the keystore paths or delete the file to get the validators picked up by the vc.~. `validator_definition.yaml` is migrated as well from the default directories. Co-authored-by: realbigsean Co-authored-by: Paul Hauner --- Cargo.lock | 18 ++++- Cargo.toml | 1 + account_manager/Cargo.toml | 1 + account_manager/src/common.rs | 24 +----- account_manager/src/lib.rs | 2 +- account_manager/src/validator/create.rs | 52 +++++++------ account_manager/src/validator/deposit.rs | 18 +---- account_manager/src/validator/import.rs | 20 +---- account_manager/src/validator/list.rs | 27 ++----- account_manager/src/validator/mod.rs | 37 +++++++--- account_manager/src/validator/recover.rs | 36 +++------ account_manager/src/wallet/create.rs | 12 +-- account_manager/src/wallet/list.rs | 8 +- account_manager/src/wallet/mod.rs | 36 +++++---- beacon_node/Cargo.toml | 1 + beacon_node/client/Cargo.toml | 1 + beacon_node/client/src/config.rs | 5 +- beacon_node/eth2_libp2p/Cargo.toml | 1 + beacon_node/eth2_libp2p/src/config.rs | 14 +++- beacon_node/src/config.rs | 21 ++++-- book/src/key-management.md | 14 ++-- book/src/validator-create.md | 10 ++- book/src/validator-management.md | 6 +- common/directory/Cargo.toml | 13 ++++ common/directory/src/lib.rs | 60 +++++++++++++++ consensus/types/Cargo.toml | 1 - .../builders/testing_beacon_state_builder.rs | 12 --- lcli/Cargo.toml | 1 + lcli/src/eth1_genesis.rs | 2 +- lcli/src/interop_genesis.rs | 2 +- lcli/src/new_testnet.rs | 2 +- lighthouse/Cargo.toml | 3 +- lighthouse/src/main.rs | 6 +- lighthouse/tests/account_manager.rs | 28 +++---- testing/node_test_rig/src/lib.rs | 8 +- validator_client/Cargo.toml | 1 + validator_client/src/cli.rs | 29 +++++++- validator_client/src/config.rs | 73 +++++++++++++------ validator_client/src/lib.rs | 10 +-- validator_client/src/validator_store.rs | 16 +++- 40 files changed, 367 insertions(+), 265 deletions(-) create mode 100644 common/directory/Cargo.toml create mode 100644 common/directory/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6049d2e7f..73c7d707a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "clap", "clap_utils", "deposit_contract", + "directory", "dirs", "environment", "eth2_keystore", @@ -377,6 +378,7 @@ dependencies = [ "clap_utils", "client", "ctrlc", + "directory", "dirs", "environment", "eth2_config", @@ -758,6 +760,7 @@ version = "0.2.0" dependencies = [ "beacon_chain", "bus", + "directory", "dirs", "environment", "error-chain", @@ -1216,6 +1219,16 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "directory" +version = "0.1.0" +dependencies = [ + "clap", + "clap_utils", + "dirs", + "eth2_testnet_config", +] + [[package]] name = "dirs" version = "2.0.2" @@ -1522,6 +1535,7 @@ name = "eth2_libp2p" version = "0.2.0" dependencies = [ "base64 0.12.3", + "directory", "dirs", "discv5", "environment", @@ -2567,6 +2581,7 @@ dependencies = [ "clap", "clap_utils", "deposit_contract", + "directory", "dirs", "environment", "eth2_keystore", @@ -2929,6 +2944,7 @@ dependencies = [ "boot_node", "clap", "clap_utils", + "directory", "env_logger", "environment", "eth2_testnet_config", @@ -5829,7 +5845,6 @@ dependencies = [ "compare_fields_derive", "criterion", "derivative", - "dirs", "eth2_hashing", "eth2_interop_keypairs", "eth2_ssz", @@ -6022,6 +6037,7 @@ dependencies = [ "clap", "clap_utils", "deposit_contract", + "directory", "dirs", "environment", "eth2_config", diff --git a/Cargo.toml b/Cargo.toml index 92fb5bccf..82922f5a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "common/compare_fields", "common/compare_fields_derive", "common/deposit_contract", + "common/directory", "common/eth2_config", "common/eth2_interop_keypairs", "common/eth2_testnet_config", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 9a533aea2..7127a2ddf 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -24,6 +24,7 @@ eth2_testnet_config = { path = "../common/eth2_testnet_config" } web3 = "0.11.0" futures = { version = "0.3.5", features = ["compat"] } clap_utils = { path = "../common/clap_utils" } +directory = { path = "../common/directory" } eth2_wallet = { path = "../crypto/eth2_wallet" } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } rand = "0.7.2" diff --git a/account_manager/src/common.rs b/account_manager/src/common.rs index 030092036..2b9c93fb1 100644 --- a/account_manager/src/common.rs +++ b/account_manager/src/common.rs @@ -1,10 +1,8 @@ use account_utils::PlainText; use account_utils::{read_input_from_user, strip_off_newlines}; -use clap::ArgMatches; use eth2_wallet::bip39::{Language, Mnemonic}; use std::fs; -use std::fs::create_dir_all; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::str::from_utf8; use std::thread::sleep; use std::time::Duration; @@ -12,26 +10,6 @@ use std::time::Duration; pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; pub const WALLET_NAME_PROMPT: &str = "Enter wallet name:"; -pub fn ensure_dir_exists>(path: P) -> Result<(), String> { - let path = path.as_ref(); - - if !path.exists() { - create_dir_all(path).map_err(|e| format!("Unable to create {:?}: {:?}", path, e))?; - } - - Ok(()) -} - -pub fn base_wallet_dir(matches: &ArgMatches, arg: &'static str) -> Result { - clap_utils::parse_path_with_default_in_home_dir( - matches, - arg, - PathBuf::new().join(".lighthouse").join("wallets"), - ) -} - -/// Reads in a mnemonic from the user. If the file path is provided, read from it. Otherwise, read -/// from an interactive prompt using tty, unless the `--stdin-inputs` flag is provided. pub fn read_mnemonic_from_cli( mnemonic_path: Option, stdin_inputs: bool, diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 5300693dc..829756778 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -10,7 +10,7 @@ use types::EthSpec; pub const CMD: &str = "account_manager"; pub const SECRETS_DIR_FLAG: &str = "secrets-dir"; pub const VALIDATOR_DIR_FLAG: &str = "validator-dir"; -pub const BASE_DIR_FLAG: &str = "base-dir"; +pub const WALLETS_DIR_FLAG: &str = "wallets-dir"; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 948942978..0d4566e46 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,10 +1,13 @@ use crate::common::read_wallet_name_from_cli; use crate::wallet::create::STDIN_INPUTS_FLAG; -use crate::{common::ensure_dir_exists, SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG}; +use crate::{SECRETS_DIR_FLAG, WALLETS_DIR_FLAG}; use account_utils::{ random_password, read_password_from_user, strip_off_newlines, validator_definitions, PlainText, }; use clap::{App, Arg, ArgMatches}; +use directory::{ + ensure_dir_exists, parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR, +}; use environment::Environment; use eth2_wallet_manager::WalletManager; use std::ffi::OsStr; @@ -14,7 +17,6 @@ use types::EthSpec; use validator_dir::Builder as ValidatorDirBuilder; pub const CMD: &str = "create"; -pub const BASE_DIR_FLAG: &str = "base-dir"; pub const WALLET_NAME_FLAG: &str = "wallet-name"; pub const WALLET_PASSWORD_FLAG: &str = "wallet-password"; pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; @@ -44,14 +46,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true), ) .arg( - Arg::with_name(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path where the validator directories will be created. \ - Defaults to ~/.lighthouse/validators", - ) - .takes_value(true), + Arg::with_name(WALLETS_DIR_FLAG) + .long(WALLETS_DIR_FLAG) + .value_name(WALLETS_DIR_FLAG) + .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{testnet}/wallets") + .takes_value(true) + .conflicts_with("datadir"), ) .arg( Arg::with_name(SECRETS_DIR_FLAG) @@ -59,8 +59,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("SECRETS_DIR") .help( "The path where the validator keystore passwords will be stored. \ - Defaults to ~/.lighthouse/secrets", + Defaults to ~/.lighthouse/{testnet}/secrets", ) + .conflicts_with("datadir") .takes_value(true), ) .arg( @@ -111,23 +112,25 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { pub fn cli_run( matches: &ArgMatches, mut env: Environment, - wallet_base_dir: PathBuf, + validator_dir: PathBuf, ) -> Result<(), String> { let spec = env.core_context().eth2_config.spec; let name: Option = clap_utils::parse_optional(matches, WALLET_NAME_FLAG)?; let stdin_inputs = matches.is_present(STDIN_INPUTS_FLAG); + let wallet_base_dir = if matches.value_of("datadir").is_some() { + let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; + path.join(DEFAULT_WALLET_DIR) + } else { + parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? + }; + let secrets_dir = if matches.value_of("datadir").is_some() { + let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; + path.join(DEFAULT_SECRET_DIR) + } else { + parse_path_or_default_with_flag(matches, SECRETS_DIR_FLAG, DEFAULT_SECRET_DIR)? + }; - let validator_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - VALIDATOR_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("validators"), - )?; - let secrets_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - SECRETS_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("secrets"), - )?; let deposit_gwei = clap_utils::parse_optional(matches, DEPOSIT_GWEI_FLAG)? .unwrap_or_else(|| spec.max_effective_balance); let count: Option = clap_utils::parse_optional(matches, COUNT_FLAG)?; @@ -136,6 +139,9 @@ pub fn cli_run( ensure_dir_exists(&validator_dir)?; ensure_dir_exists(&secrets_dir)?; + eprintln!("secrets-dir path {:?}", secrets_dir); + eprintln!("wallets-dir path {:?}", wallet_base_dir); + let starting_validator_count = existing_validator_count(&validator_dir)?; let n = match (count, at_most) { @@ -166,7 +172,7 @@ pub fn cli_run( let wallet_password = read_wallet_password_from_cli(wallet_password_path, stdin_inputs)?; let mgr = WalletManager::open(&wallet_base_dir) - .map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?; + .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; let mut wallet = mgr .wallet_by_name(&wallet_name) diff --git a/account_manager/src/validator/deposit.rs b/account_manager/src/validator/deposit.rs index 0e508cfd2..233e7634e 100644 --- a/account_manager/src/validator/deposit.rs +++ b/account_manager/src/validator/deposit.rs @@ -46,16 +46,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { The deposit contract address will be determined by the --testnet-dir flag on the \ primary Lighthouse binary.", ) - .arg( - Arg::with_name(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path to the validator client data directory. \ - Defaults to ~/.lighthouse/validators", - ) - .takes_value(true), - ) .arg( Arg::with_name(VALIDATOR_FLAG) .long(VALIDATOR_FLAG) @@ -209,14 +199,10 @@ where pub fn cli_run( matches: &ArgMatches<'_>, mut env: Environment, + validator_dir: PathBuf, ) -> Result<(), String> { let log = env.core_context().log().clone(); - let data_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - VALIDATOR_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("validators"), - )?; let validator: String = clap_utils::parse_required(matches, VALIDATOR_FLAG)?; let eth1_ipc_path: Option = clap_utils::parse_optional(matches, ETH1_IPC_FLAG)?; let eth1_http_url: Option = clap_utils::parse_optional(matches, ETH1_HTTP_FLAG)?; @@ -225,7 +211,7 @@ pub fn cli_run( let confirmation_batch_size: usize = clap_utils::parse_required(matches, CONFIRMATION_BATCH_SIZE_FLAG)?; - let manager = ValidatorManager::open(&data_dir) + let manager = ValidatorManager::open(&validator_dir) .map_err(|e| format!("Unable to read --{}: {:?}", VALIDATOR_DIR_FLAG, e))?; let validators = match validator.as_ref() { diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 5216b3d9c..1998709d2 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -1,5 +1,4 @@ use crate::wallet::create::STDIN_INPUTS_FLAG; -use crate::{common::ensure_dir_exists, VALIDATOR_DIR_FLAG}; use account_utils::{ eth2_keystore::Keystore, read_password_from_user, @@ -55,16 +54,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .required_unless(KEYSTORE_FLAG) .takes_value(true), ) - .arg( - Arg::with_name(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path where the validator directories will be created. \ - Defaults to ~/.lighthouse/validators", - ) - .takes_value(true), - ) .arg( Arg::with_name(STDIN_INPUTS_FLAG) .long(STDIN_INPUTS_FLAG) @@ -77,19 +66,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { +pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { let keystore: Option = clap_utils::parse_optional(matches, KEYSTORE_FLAG)?; let keystores_dir: Option = clap_utils::parse_optional(matches, DIR_FLAG)?; - let validator_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - VALIDATOR_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("validators"), - )?; let stdin_inputs = matches.is_present(STDIN_INPUTS_FLAG); let reuse_password = matches.is_present(REUSE_PASSWORD_FLAG); - ensure_dir_exists(&validator_dir)?; - let mut defs = ValidatorDefinitions::open_or_create(&validator_dir) .map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?; diff --git a/account_manager/src/validator/list.rs b/account_manager/src/validator/list.rs index 148564303..dd97de156 100644 --- a/account_manager/src/validator/list.rs +++ b/account_manager/src/validator/list.rs @@ -1,38 +1,21 @@ use crate::VALIDATOR_DIR_FLAG; -use clap::{App, Arg, ArgMatches}; +use clap::App; use std::path::PathBuf; use validator_dir::Manager as ValidatorManager; pub const CMD: &str = "list"; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { - App::new(CMD) - .arg( - Arg::with_name(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path to search for validator directories. \ - Defaults to ~/.lighthouse/validators", - ) - .takes_value(true), - ) - .about("Lists the names of all validators.") + App::new(CMD).about("Lists the names of all validators.") } -pub fn cli_run(matches: &ArgMatches<'_>) -> Result<(), String> { - let data_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - VALIDATOR_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("validators"), - )?; - - let mgr = ValidatorManager::open(&data_dir) +pub fn cli_run(validator_dir: PathBuf) -> Result<(), String> { + let mgr = ValidatorManager::open(&validator_dir) .map_err(|e| format!("Unable to read --{}: {:?}", VALIDATOR_DIR_FLAG, e))?; for (name, _path) in mgr .directory_names() - .map_err(|e| format!("Unable to list wallets: {:?}", e))? + .map_err(|e| format!("Unable to list validators: {:?}", e))? { println!("{}", name) } diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 84ad6df39..4c650dad0 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -4,9 +4,11 @@ pub mod import; pub mod list; pub mod recover; -use crate::common::base_wallet_dir; +use crate::VALIDATOR_DIR_FLAG; use clap::{App, Arg, ArgMatches}; +use directory::{parse_path_or_default_with_flag, DEFAULT_VALIDATOR_DIR}; use environment::Environment; +use std::path::PathBuf; use types::EthSpec; pub const CMD: &str = "validator"; @@ -15,11 +17,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about("Provides commands for managing Eth2 validators.") .arg( - Arg::with_name("base-dir") - .long("base-dir") - .value_name("BASE_DIRECTORY") - .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/wallets") - .takes_value(true), + Arg::with_name(VALIDATOR_DIR_FLAG) + .long(VALIDATOR_DIR_FLAG) + .value_name("VALIDATOR_DIRECTORY") + .help( + "The path to search for validator directories. \ + Defaults to ~/.lighthouse/{testnet}/validators", + ) + .takes_value(true) + .global(true) + .conflicts_with("datadir"), ) .subcommand(create::cli_app()) .subcommand(deposit::cli_app()) @@ -29,14 +36,20 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { - let base_wallet_dir = base_wallet_dir(matches, "base-dir")?; + let validator_base_dir = if matches.value_of("datadir").is_some() { + let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; + path.join(DEFAULT_VALIDATOR_DIR) + } else { + parse_path_or_default_with_flag(matches, VALIDATOR_DIR_FLAG, DEFAULT_VALIDATOR_DIR)? + }; + eprintln!("validator-dir path: {:?}", validator_base_dir); match matches.subcommand() { - (create::CMD, Some(matches)) => create::cli_run::(matches, env, base_wallet_dir), - (deposit::CMD, Some(matches)) => deposit::cli_run::(matches, env), - (import::CMD, Some(matches)) => import::cli_run(matches), - (list::CMD, Some(matches)) => list::cli_run(matches), - (recover::CMD, Some(matches)) => recover::cli_run(matches), + (create::CMD, Some(matches)) => create::cli_run::(matches, env, validator_base_dir), + (deposit::CMD, Some(matches)) => deposit::cli_run::(matches, env, validator_base_dir), + (import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir), + (list::CMD, Some(_)) => list::cli_run(validator_base_dir), + (recover::CMD, Some(matches)) => recover::cli_run(matches, validator_base_dir), (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index 376c21645..e3844d500 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,11 +1,13 @@ use super::create::STORE_WITHDRAW_FLAG; -use crate::common::{ensure_dir_exists, read_mnemonic_from_cli}; +use crate::common::read_mnemonic_from_cli; use crate::validator::create::COUNT_FLAG; use crate::wallet::create::STDIN_INPUTS_FLAG; -use crate::{SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG}; +use crate::SECRETS_DIR_FLAG; use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; use account_utils::random_password; use clap::{App, Arg, ArgMatches}; +use directory::ensure_dir_exists; +use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; use eth2_wallet::bip39::Seed; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; use std::path::PathBuf; @@ -48,23 +50,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .takes_value(true) ) - .arg( - Arg::with_name(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path where the validator directories will be created. \ - Defaults to ~/.lighthouse/validators", - ) - .takes_value(true), - ) .arg( Arg::with_name(SECRETS_DIR_FLAG) .long(SECRETS_DIR_FLAG) .value_name("SECRETS_DIR") .help( "The path where the validator keystore passwords will be stored. \ - Defaults to ~/.lighthouse/secrets", + Defaults to ~/.lighthouse/{testnet}/secrets", ) .takes_value(true), ) @@ -84,17 +76,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { - let validator_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - VALIDATOR_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("validators"), - )?; - let secrets_dir = clap_utils::parse_path_with_default_in_home_dir( - matches, - SECRETS_DIR_FLAG, - PathBuf::new().join(".lighthouse").join("secrets"), - )?; +pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { + let secrets_dir = if matches.value_of("datadir").is_some() { + let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; + path.join(DEFAULT_SECRET_DIR) + } else { + parse_path_or_default_with_flag(matches, SECRETS_DIR_FLAG, DEFAULT_SECRET_DIR)? + }; let first_index: u32 = clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?; let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?; let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index 04d141b48..a769cc019 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -1,5 +1,5 @@ use crate::common::read_wallet_name_from_cli; -use crate::BASE_DIR_FLAG; +use crate::WALLETS_DIR_FLAG; use account_utils::{ is_password_sufficiently_complex, random_password, read_password_from_user, strip_off_newlines, }; @@ -80,7 +80,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> { +pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { let mnemonic_output_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; // Create a new random mnemonic. @@ -88,7 +88,7 @@ pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> { // The `tiny-bip39` crate uses `thread_rng()` for this entropy. let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); - let wallet = create_wallet_from_mnemonic(matches, &base_dir.as_path(), &mnemonic)?; + let wallet = create_wallet_from_mnemonic(matches, &wallet_base_dir.as_path(), &mnemonic)?; if let Some(path) = mnemonic_output_path { create_with_600_perms(&path, mnemonic.phrase().as_bytes()) @@ -121,7 +121,7 @@ pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> { pub fn create_wallet_from_mnemonic( matches: &ArgMatches, - base_dir: &Path, + wallet_base_dir: &Path, mnemonic: &Mnemonic, ) -> Result { let name: Option = clap_utils::parse_optional(matches, NAME_FLAG)?; @@ -134,8 +134,8 @@ pub fn create_wallet_from_mnemonic( unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)), }; - let mgr = WalletManager::open(&base_dir) - .map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?; + let mgr = WalletManager::open(&wallet_base_dir) + .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; let wallet_password: PlainText = match wallet_password_path { Some(path) => { diff --git a/account_manager/src/wallet/list.rs b/account_manager/src/wallet/list.rs index 85096dc5f..5b671b1dc 100644 --- a/account_manager/src/wallet/list.rs +++ b/account_manager/src/wallet/list.rs @@ -1,4 +1,4 @@ -use crate::BASE_DIR_FLAG; +use crate::WALLETS_DIR_FLAG; use clap::App; use eth2_wallet_manager::WalletManager; use std::path::PathBuf; @@ -9,9 +9,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD).about("Lists the names of all wallets.") } -pub fn cli_run(base_dir: PathBuf) -> Result<(), String> { - let mgr = WalletManager::open(&base_dir) - .map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?; +pub fn cli_run(wallet_base_dir: PathBuf) -> Result<(), String> { + let mgr = WalletManager::open(&wallet_base_dir) + .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; for (name, _uuid) in mgr .wallets() diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index e8315b77a..d745cbcd2 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -2,11 +2,10 @@ pub mod create; pub mod list; pub mod recover; -use crate::{ - common::{base_wallet_dir, ensure_dir_exists}, - BASE_DIR_FLAG, -}; +use crate::WALLETS_DIR_FLAG; use clap::{App, Arg, ArgMatches}; +use directory::{ensure_dir_exists, parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; +use std::path::PathBuf; pub const CMD: &str = "wallet"; @@ -14,11 +13,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about("Manage wallets, from which validator keys can be derived.") .arg( - Arg::with_name(BASE_DIR_FLAG) - .long(BASE_DIR_FLAG) - .value_name("BASE_DIRECTORY") - .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/wallets") - .takes_value(true), + Arg::with_name(WALLETS_DIR_FLAG) + .long(WALLETS_DIR_FLAG) + .value_name("WALLETS_DIRECTORY") + .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{testnet}/wallets") + .takes_value(true) + .global(true) + .conflicts_with("datadir"), ) .subcommand(create::cli_app()) .subcommand(list::cli_app()) @@ -26,13 +27,20 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { - let base_dir = base_wallet_dir(matches, BASE_DIR_FLAG)?; - ensure_dir_exists(&base_dir)?; + let wallet_base_dir = if matches.value_of("datadir").is_some() { + let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; + path.join(DEFAULT_WALLET_DIR) + } else { + parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? + }; + ensure_dir_exists(&wallet_base_dir)?; + + eprintln!("wallet-dir path: {:?}", wallet_base_dir); match matches.subcommand() { - (create::CMD, Some(matches)) => create::cli_run(matches, base_dir), - (list::CMD, Some(_)) => list::cli_run(base_dir), - (recover::CMD, Some(matches)) => recover::cli_run(matches, base_dir), + (create::CMD, Some(matches)) => create::cli_run(matches, wallet_base_dir), + (list::CMD, Some(_)) => list::cli_run(wallet_base_dir), + (recover::CMD, Some(matches)) => recover::cli_run(matches, wallet_base_dir), (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 0351b1cb4..deb965af1 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -30,6 +30,7 @@ tokio = { version = "0.2.21", features = ["time"] } exit-future = "0.2.0" dirs = "2.0.2" logging = { path = "../common/logging" } +directory = {path = "../common/directory"} futures = "0.3.5" environment = { path = "../lighthouse/environment" } genesis = { path = "genesis" } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index de6f7e59d..ba98eb946 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -41,3 +41,4 @@ lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } time = "0.2.16" bus = "2.2.3" +directory = {path = "../../common/directory"} diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 19088e785..fdcd3d6e8 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -1,11 +1,10 @@ +use directory::DEFAULT_ROOT_DIR; use network::NetworkConfig; use serde_derive::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use types::Graffiti; -pub const DEFAULT_DATADIR: &str = ".lighthouse"; - /// The number initial validators when starting the `Minimal`. const TESTNET_SPEC_CONSTANTS: &str = "minimal"; @@ -72,7 +71,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - data_dir: PathBuf::from(DEFAULT_DATADIR), + data_dir: PathBuf::from(DEFAULT_ROOT_DIR), db_name: "chain_db".to_string(), freezer_db_path: None, log_file: PathBuf::from(""), diff --git a/beacon_node/eth2_libp2p/Cargo.toml b/beacon_node/eth2_libp2p/Cargo.toml index 2df5123b5..de916f8fa 100644 --- a/beacon_node/eth2_libp2p/Cargo.toml +++ b/beacon_node/eth2_libp2p/Cargo.toml @@ -36,6 +36,7 @@ discv5 = { version = "0.1.0-alpha.12", features = ["libp2p"] } tiny-keccak = "2.0.2" environment = { path = "../../lighthouse/environment" } rand = "0.7.3" +directory = { path = "../../common/directory" } regex = "1.3.9" [dependencies.libp2p] diff --git a/beacon_node/eth2_libp2p/src/config.rs b/beacon_node/eth2_libp2p/src/config.rs index 73094642d..11bb0d362 100644 --- a/beacon_node/eth2_libp2p/src/config.rs +++ b/beacon_node/eth2_libp2p/src/config.rs @@ -1,5 +1,8 @@ use crate::types::GossipKind; use crate::{Enr, PeerIdSerialized}; +use directory::{ + DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_TESTNET, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, +}; use discv5::{Discv5Config, Discv5ConfigBuilder}; use libp2p::gossipsub::{ GossipsubConfig, GossipsubConfigBuilder, GossipsubMessage, MessageId, ValidationMode, @@ -74,9 +77,14 @@ pub struct Config { impl Default for Config { /// Generate a default network configuration. fn default() -> Self { - let mut network_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - network_dir.push(".lighthouse"); - network_dir.push("network"); + // WARNING: this directory default should be always overrided with parameters + // from cli for specific networks. + let network_dir = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(DEFAULT_ROOT_DIR) + .join(DEFAULT_HARDCODED_TESTNET) + .join(DEFAULT_BEACON_NODE_DIR) + .join(DEFAULT_NETWORK_DIR); // The function used to generate a gossipsub message id // We use the first 8 bytes of SHA256(data) for content addressing diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 42b3b8277..aabdbb35c 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1,7 +1,8 @@ use beacon_chain::builder::PUBKEY_CACHE_FILENAME; use clap::ArgMatches; use clap_utils::BAD_TESTNET_DIR_MESSAGE; -use client::{config::DEFAULT_DATADIR, ClientConfig, ClientGenesis}; +use client::{ClientConfig, ClientGenesis}; +use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use eth2_libp2p::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; use eth2_testnet_config::Eth2TestnetConfig; use slog::{crit, info, warn, Logger}; @@ -13,9 +14,6 @@ use std::net::{TcpListener, UdpSocket}; use std::path::PathBuf; use types::{ChainSpec, EthSpec, GRAFFITI_BYTES_LEN}; -pub const BEACON_NODE_DIR: &str = "beacon"; -pub const NETWORK_DIR: &str = "network"; - /// Gets the fully-initialized global client. /// /// The top-level `clap` arguments should be provided as `cli_args`. @@ -295,7 +293,7 @@ pub fn set_network_config( if let Some(dir) = cli_args.value_of("network-dir") { config.network_dir = PathBuf::from(dir); } else { - config.network_dir = data_dir.join(NETWORK_DIR); + config.network_dir = data_dir.join(DEFAULT_NETWORK_DIR); }; if let Some(listen_address_str) = cli_args.value_of("listen-address") { @@ -456,11 +454,18 @@ pub fn get_data_dir(cli_args: &ArgMatches) -> PathBuf { // Read the `--datadir` flag. // // If it's not present, try and find the home directory (`~`) and push the default data - // directory onto it. + // directory and the testnet name onto it. + cli_args .value_of("datadir") - .map(|path| PathBuf::from(path).join(BEACON_NODE_DIR)) - .or_else(|| dirs::home_dir().map(|home| home.join(DEFAULT_DATADIR).join(BEACON_NODE_DIR))) + .map(|path| PathBuf::from(path).join(DEFAULT_BEACON_NODE_DIR)) + .or_else(|| { + dirs::home_dir().map(|home| { + home.join(DEFAULT_ROOT_DIR) + .join(directory::get_testnet_name(cli_args)) + .join(DEFAULT_BEACON_NODE_DIR) + }) + }) .unwrap_or_else(|| PathBuf::from(".")) } diff --git a/book/src/key-management.md b/book/src/key-management.md index 53edec221..4b03bec0e 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -40,12 +40,12 @@ keypairs. Creating a single validator looks like this: - `lighthouse account validator create --wallet-name wally --wallet-password wally.pass --count 1` -In step (1), we created a wallet in `~/.lighthouse/wallets` with the name +In step (1), we created a wallet in `~/.lighthouse/{testnet}/wallets` with the name `wally`. We encrypted this using a pre-defined password in the `wally.pass` file. Then, in step (2), we created one new validator in the -`~/.lighthouse/validators` directory using `wally` (unlocking it with +`~/.lighthouse/{testnet}/validators` directory using `wally` (unlocking it with `wally.pass`) and storing the passwords to the validators voting key in -`~/.lighthouse/secrets`. +`~/.lighthouse/{testnet}/secrets`. Thanks to the hierarchical key derivation scheme, we can delete all of the aforementioned directories and then regenerate them as long as we remembered @@ -63,14 +63,16 @@ There are three important directories in Lighthouse validator key management: - `wallets/`: contains encrypted wallets which are used for hierarchical key derivation. - - Defaults to `~/.lighthouse/wallets` + - Defaults to `~/.lighthouse/{testnet}/wallets` - `validators/`: contains a directory for each validator containing encrypted keystores and other validator-specific data. - - Defaults to `~/.lighthouse/validators` + - Defaults to `~/.lighthouse/{testnet}/validators` - `secrets/`: since the validator signing keys are "hot", the validator process needs access to the passwords to decrypt the keystores in the validators dir. These passwords are stored here. - - Defaults to `~/.lighthouse/secrets` + - Defaults to `~/.lighthouse/{testnet}/secrets` + +where `testnet` is the name of the testnet passed in the `--testnet` parameter (default is `medalla`). When the validator client boots, it searches the `validators/` for directories containing voting keystores. When it discovers a keystore, it searches the diff --git a/book/src/validator-create.md b/book/src/validator-create.md index 25112e748..9d73cdf80 100644 --- a/book/src/validator-create.md +++ b/book/src/validator-create.md @@ -41,7 +41,7 @@ OPTIONS: The GWEI value of the deposit amount. Defaults to the minimum amount required for an active validator (MAX_EFFECTIVE_BALANCE) --secrets-dir - The path where the validator keystore passwords will be stored. Defaults to ~/.lighthouse/secrets + The path where the validator keystore passwords will be stored. Defaults to ~/.lighthouse/{testnet}/secrets -s, --spec Specifies the default eth2 spec type. [default: mainnet] [possible values: mainnet, minimal, interop] @@ -53,7 +53,7 @@ OPTIONS: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. --validator-dir <VALIDATOR_DIRECTORY> - The path where the validator directories will be created. Defaults to ~/.lighthouse/validators + The path where the validator directories will be created. Defaults to ~/.lighthouse/{testnet}/validators --wallet-name <WALLET_NAME> Use the wallet identified by this name --wallet-password <WALLET_PASSWORD_PATH> @@ -73,10 +73,12 @@ This command will: - Derive a single new BLS keypair from `wally`, updating it so that it generates a new key next time. -- Create a new directory in `~/.lighthouse/validators` containing: +- Create a new directory in `~/.lighthouse/{testnet}/validators` containing: - An encrypted keystore containing the validators voting keypair. - An `eth1_deposit_data.rlp` assuming the default deposit amount (`32 ETH` for most testnets and mainnet) which can be submitted to the deposit contract for the medalla testnet. Other testnets can be set via the `--testnet` CLI param. -- Store a password to the validators voting keypair in `~/.lighthouse/secrets`. +- Store a password to the validators voting keypair in `~/.lighthouse/{testnet}/secrets`. + +where `testnet` is the name of the testnet passed in the `--testnet` parameter (default is `medalla`). \ No newline at end of file diff --git a/book/src/validator-management.md b/book/src/validator-management.md index fbb76c9b4..df0e7243d 100644 --- a/book/src/validator-management.md +++ b/book/src/validator-management.md @@ -16,7 +16,7 @@ useful. ## Introducing the `validator_definitions.yml` file The `validator_definitions.yml` file is located in the `validator-dir`, which -defaults to `~/.lighthouse/validators`. It is a +defaults to `~/.lighthouse/{testnet}/validators`. It is a [YAML](https://en.wikipedia.org/wiki/YAML) encoded file defining exactly which validators the validator client will (and won't) act for. @@ -92,7 +92,7 @@ name identical to the `voting_public_key` value. Lets assume the following directory structure: ``` -~/.lighthouse/validators +~/.lighthouse/{testnet}/validators ├── john │   └── voting-keystore.json ├── sally @@ -135,7 +135,7 @@ In order for the validator client to decrypt the validators, they will need to ensure their `secrets-dir` is organised as below: ``` -~/.lighthouse/secrets +~/.lighthouse/{testnet}/secrets ├── 0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477 ├── 0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337 └── 0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007 diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml new file mode 100644 index 000000000..ebea5f3dc --- /dev/null +++ b/common/directory/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "directory" +version = "0.1.0" +authors = ["pawan <pawandhananjay@gmail.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = "2.33.0" +clap_utils = {path = "../clap_utils"} +dirs = "2.0.2" +eth2_testnet_config = { path = "../eth2_testnet_config" } diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs new file mode 100644 index 000000000..765fdabd6 --- /dev/null +++ b/common/directory/src/lib.rs @@ -0,0 +1,60 @@ +use clap::ArgMatches; +pub use eth2_testnet_config::DEFAULT_HARDCODED_TESTNET; +use std::fs::create_dir_all; +use std::path::{Path, PathBuf}; + +/// Names for the default directories. +pub const DEFAULT_ROOT_DIR: &str = ".lighthouse"; +pub const DEFAULT_BEACON_NODE_DIR: &str = "beacon"; +pub const DEFAULT_NETWORK_DIR: &str = "network"; +pub const DEFAULT_VALIDATOR_DIR: &str = "validators"; +pub const DEFAULT_SECRET_DIR: &str = "secrets"; +pub const DEFAULT_WALLET_DIR: &str = "wallets"; + +/// Base directory name for unnamed testnets passed through the --testnet-dir flag +pub const CUSTOM_TESTNET_DIR: &str = "custom"; + +/// Gets the testnet directory name +/// +/// Tries to get the name first from the "testnet" flag, +/// if not present, then checks the "testnet-dir" flag and returns a custom name +/// If neither flags are present, returns the default hardcoded network name. +pub fn get_testnet_name(matches: &ArgMatches) -> String { + if let Some(testnet_name) = matches.value_of("testnet") { + testnet_name.to_string() + } else if matches.value_of("testnet-dir").is_some() { + CUSTOM_TESTNET_DIR.to_string() + } else { + eth2_testnet_config::DEFAULT_HARDCODED_TESTNET.to_string() + } +} + +/// Checks if a directory exists in the given path and creates a directory if it does not exist. +pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> Result<(), String> { + let path = path.as_ref(); + + if !path.exists() { + create_dir_all(path).map_err(|e| format!("Unable to create {:?}: {:?}", path, e))?; + } + + Ok(()) +} + +/// If `arg` is in `matches`, parses the value as a path. +/// +/// Otherwise, attempts to find the default directory for the `testnet` from the `matches` +/// and appends `flag` to it. +pub fn parse_path_or_default_with_flag( + matches: &ArgMatches, + arg: &'static str, + flag: &str, +) -> Result<PathBuf, String> { + clap_utils::parse_path_with_default_in_home_dir( + matches, + arg, + PathBuf::new() + .join(DEFAULT_ROOT_DIR) + .join(get_testnet_name(matches)) + .join(flag), + ) +} diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 8f6fed4b4..80b4007b9 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -12,7 +12,6 @@ harness = false bls = { path = "../../crypto/bls" } compare_fields = { path = "../../common/compare_fields" } compare_fields_derive = { path = "../../common/compare_fields_derive" } -dirs = "2.0.2" eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } ethereum-types = "0.9.1" eth2_hashing = "0.1.0" diff --git a/consensus/types/src/test_utils/builders/testing_beacon_state_builder.rs b/consensus/types/src/test_utils/builders/testing_beacon_state_builder.rs index 67a3dae26..922d4017f 100644 --- a/consensus/types/src/test_utils/builders/testing_beacon_state_builder.rs +++ b/consensus/types/src/test_utils/builders/testing_beacon_state_builder.rs @@ -4,21 +4,9 @@ use crate::*; use bls::get_withdrawal_credentials; use log::debug; use rayon::prelude::*; -use std::path::PathBuf; pub const KEYPAIRS_FILE: &str = "keypairs.raw_keypairs"; -/// Returns the directory where the generated keypairs should be stored. -/// -/// It is either `$HOME/.lighthouse/keypairs.raw_keypairs` or, if `$HOME` is not available, -/// `./keypairs.raw_keypairs`. -pub fn keypairs_path() -> PathBuf { - let dir = dirs::home_dir() - .map(|home| (home.join(".lighthouse"))) - .unwrap_or_else(|| PathBuf::from("")); - dir.join(KEYPAIRS_FILE) -} - /// Builds a beacon state to be used for testing purposes. /// /// This struct should **never be used for production purposes.** diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index c2c2c09b8..8872922bb 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -35,3 +35,4 @@ validator_dir = { path = "../common/validator_dir", features = ["insecure_keys"] rand = "0.7.2" eth2_keystore = { path = "../crypto/eth2_keystore" } lighthouse_version = { path = "../common/lighthouse_version" } +directory = { path = "../common/directory" } diff --git a/lcli/src/eth1_genesis.rs b/lcli/src/eth1_genesis.rs index 2c6f7d8cf..9fd0757d8 100644 --- a/lcli/src/eth1_genesis.rs +++ b/lcli/src/eth1_genesis.rs @@ -20,7 +20,7 @@ pub fn run<T: EthSpec>(mut env: Environment<T>, matches: &ArgMatches<'_>) -> Res .and_then(|dir| dir.parse::<PathBuf>().map_err(|_| ())) .unwrap_or_else(|_| { dirs::home_dir() - .map(|home| home.join(".lighthouse").join("testnet")) + .map(|home| home.join(directory::DEFAULT_ROOT_DIR).join("testnet")) .expect("should locate home directory") }); diff --git a/lcli/src/interop_genesis.rs b/lcli/src/interop_genesis.rs index 9c8609b5c..28cd2625b 100644 --- a/lcli/src/interop_genesis.rs +++ b/lcli/src/interop_genesis.rs @@ -31,7 +31,7 @@ pub fn run<T: EthSpec>(mut env: Environment<T>, matches: &ArgMatches) -> Result< .and_then(|dir| dir.parse::<PathBuf>().map_err(|_| ())) .unwrap_or_else(|_| { dirs::home_dir() - .map(|home| home.join(".lighthouse").join("testnet")) + .map(|home| home.join(directory::DEFAULT_ROOT_DIR).join("testnet")) .expect("should locate home directory") }); diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs index 918426e74..fc60e8c98 100644 --- a/lcli/src/new_testnet.rs +++ b/lcli/src/new_testnet.rs @@ -10,7 +10,7 @@ pub fn run<T: EthSpec>(matches: &ArgMatches) -> Result<(), String> { let testnet_dir_path = parse_path_with_default_in_home_dir( matches, "testnet-dir", - PathBuf::from(".lighthouse/testnet"), + PathBuf::from(directory::DEFAULT_ROOT_DIR).join("testnet"), )?; let deposit_contract_address: Address = parse_required(matches, "deposit-contract-address")?; let deposit_contract_deploy_block = parse_required(matches, "deposit-contract-deploy-block")?; diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 3bc232d9f..1daf5f97c 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -31,9 +31,10 @@ validator_client = { "path" = "../validator_client" } account_manager = { "path" = "../account_manager" } clap_utils = { path = "../common/clap_utils" } eth2_testnet_config = { path = "../common/eth2_testnet_config" } +directory = { path = "../common/directory" } lighthouse_version = { path = "../common/lighthouse_version" } +account_utils = { path = "../common/account_utils" } [dev-dependencies] tempfile = "3.1.0" validator_dir = { path = "../common/validator_dir" } -account_utils = { path = "../common/account_utils" } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index c174992e0..9d13706a1 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -10,7 +10,6 @@ use std::process::exit; use types::EthSpec; use validator_client::ProductionValidatorClient; -pub const DEFAULT_DATA_DIR: &str = ".lighthouse"; pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; fn bls_library_name() -> &'static str { @@ -91,7 +90,10 @@ fn main() { .short("d") .value_name("DIR") .global(true) - .help("Data directory for lighthouse keys and databases.") + .help( + "Root data directory for lighthouse keys and databases. \ + Defaults to $HOME/.lighthouse/{default-testnet}, \ + currently, $HOME/.lighthouse/medalla") .takes_value(true), ) .arg( diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index f5c473034..30f885b4e 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -11,7 +11,7 @@ use account_manager::{ list::CMD as LIST_CMD, CMD as WALLET_CMD, }, - BASE_DIR_FLAG, CMD as ACCOUNT_CMD, *, + CMD as ACCOUNT_CMD, WALLETS_DIR_FLAG, *, }; use account_utils::{ eth2_keystore::KeystoreBuilder, @@ -73,7 +73,7 @@ fn dir_child_count<P: AsRef<Path>>(dir: P) -> usize { fn list_wallets<P: AsRef<Path>>(base_dir: P) -> Vec<String> { let output = output_result( wallet_cmd() - .arg(format!("--{}", BASE_DIR_FLAG)) + .arg(format!("--{}", WALLETS_DIR_FLAG)) .arg(base_dir.as_ref().as_os_str()) .arg(LIST_CMD), ) @@ -97,7 +97,7 @@ fn create_wallet<P: AsRef<Path>>( ) -> Result<Output, String> { output_result( wallet_cmd() - .arg(format!("--{}", BASE_DIR_FLAG)) + .arg(format!("--{}", WALLETS_DIR_FLAG)) .arg(base_dir.as_ref().as_os_str()) .arg(CREATE_CMD) .arg(format!("--{}", NAME_FLAG)) @@ -233,15 +233,15 @@ impl TestValidator { store_withdrawal_key: bool, ) -> Result<Vec<String>, String> { let mut cmd = validator_cmd(); - cmd.arg(format!("--{}", BASE_DIR_FLAG)) - .arg(self.wallet.base_dir().into_os_string()) + cmd.arg(format!("--{}", VALIDATOR_DIR_FLAG)) + .arg(self.validator_dir.clone().into_os_string()) .arg(CREATE_CMD) + .arg(format!("--{}", WALLETS_DIR_FLAG)) + .arg(self.wallet.base_dir().into_os_string()) .arg(format!("--{}", WALLET_NAME_FLAG)) .arg(&self.wallet.name) .arg(format!("--{}", WALLET_PASSWORD_FLAG)) .arg(self.wallet.password_path().into_os_string()) - .arg(format!("--{}", VALIDATOR_DIR_FLAG)) - .arg(self.validator_dir.clone().into_os_string()) .arg(format!("--{}", SECRETS_DIR_FLAG)) .arg(self.secrets_dir.clone().into_os_string()) .arg(format!("--{}", DEPOSIT_GWEI_FLAG)) @@ -375,13 +375,6 @@ fn validator_create() { assert_eq!(dir_child_count(validator_dir.path()), 6); } -/// Returns the `lighthouse account validator import` command. -fn validator_import_cmd() -> Command { - let mut cmd = validator_cmd(); - cmd.arg(IMPORT_CMD); - cmd -} - #[test] fn validator_import_launchpad() { const PASSWORD: &str = "cats"; @@ -407,12 +400,13 @@ fn validator_import_launchpad() { // Create a not-keystore file in the src dir. File::create(src_dir.path().join(NOT_KEYSTORE_NAME)).unwrap(); - let mut child = validator_import_cmd() + let mut child = validator_cmd() + .arg(format!("--{}", VALIDATOR_DIR_FLAG)) + .arg(dst_dir.path().as_os_str()) + .arg(IMPORT_CMD) .arg(format!("--{}", STDIN_INPUTS_FLAG)) // Using tty does not work well with tests. .arg(format!("--{}", import::DIR_FLAG)) .arg(src_dir.path().as_os_str()) - .arg(format!("--{}", VALIDATOR_DIR_FLAG)) - .arg(dst_dir.path().as_os_str()) .stderr(Stdio::piped()) .stdin(Stdio::piped()) .spawn() diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 9459a07b5..b1a74b64a 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -96,7 +96,7 @@ pub fn testing_client_config() -> ClientConfig { /// This struct is separate to `LocalValidatorClient` to allow for pre-computation of validator /// keypairs since the task is quite resource intensive. pub struct ValidatorFiles { - pub datadir: TempDir, + pub validator_dir: TempDir, pub secrets_dir: TempDir, } @@ -110,7 +110,7 @@ impl ValidatorFiles { .map_err(|e| format!("Unable to create VC secrets dir: {:?}", e))?; Ok(Self { - datadir, + validator_dir: datadir, secrets_dir, }) } @@ -120,7 +120,7 @@ impl ValidatorFiles { let this = Self::new()?; build_deterministic_validator_dirs( - this.datadir.path().into(), + this.validator_dir.path().into(), this.secrets_dir.path().into(), keypair_indices, ) @@ -170,7 +170,7 @@ impl<E: EthSpec> LocalValidatorClient<E> { mut config: ValidatorConfig, files: ValidatorFiles, ) -> Result<Self, String> { - config.data_dir = files.datadir.path().into(); + config.validator_dir = files.validator_dir.path().into(); config.secrets_dir = files.secrets_dir.path().into(); ProductionValidatorClient::new(context, config) diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 1bbde0c5a..77a6e5ce9 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -31,6 +31,7 @@ slog-term = "2.5.0" tokio = { version = "0.2.21", features = ["time"] } futures = { version = "0.3.5", features = ["compat"] } dirs = "2.0.2" +directory = {path = "../common/directory"} logging = { path = "../common/logging" } environment = { path = "../lighthouse/environment" } parking_lot = "0.11.0" diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 7ac483439..9ad0c3faa 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -16,6 +16,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value(&DEFAULT_HTTP_SERVER) .takes_value(true), ) + .arg( + Arg::with_name("validators-dir") + .long("validators-dir") + .value_name("VALIDATORS_DIR") + .help( + "The directory which contains the validator keystores, deposit data for \ + each validator along with the common slashing protection database \ + and the validator_definitions.yml" + ) + .takes_value(true) + .conflicts_with("datadir") + .requires("secrets-dir") + ) .arg( Arg::with_name("secrets-dir") .long("secrets-dir") @@ -24,9 +37,11 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { "The directory which contains the password to unlock the validator \ voting keypairs. Each password should be contained in a file where the \ name is the 0x-prefixed hex representation of the validators voting public \ - key. Defaults to ~/.lighthouse/secrets.", + key. Defaults to ~/.lighthouse/{testnet}/secrets.", ) - .takes_value(true), + .takes_value(true) + .conflicts_with("datadir") + .requires("validators-dir"), ) .arg(Arg::with_name("auto-register").long("auto-register").help( "If present, the validator client will register any new signing keys with \ @@ -48,6 +63,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { that might also be using the same keystores." ) ) + .arg( + Arg::with_name("strict-slashing-protection") + .long("strict-slashing-protection") + .help( + "If present, do not create a new slashing database. This is to ensure that users \ + do not accidentally get slashed in case their slashing protection db ends up in the \ + wrong directory during directory restructure and vc creates a new empty db and \ + re-registers all validators." + ) + ) .arg( Arg::with_name("disable-auto-discover") .long("disable-auto-discover") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 4a11c5aec..991b55162 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,12 +1,14 @@ use clap::ArgMatches; -use clap_utils::{parse_optional, parse_path_with_default_in_home_dir}; +use clap_utils::{parse_optional, parse_required}; +use directory::{ + get_testnet_name, DEFAULT_HARDCODED_TESTNET, DEFAULT_ROOT_DIR, DEFAULT_SECRET_DIR, + DEFAULT_VALIDATOR_DIR, +}; use serde_derive::{Deserialize, Serialize}; use std::path::PathBuf; use types::{Graffiti, GRAFFITI_BYTES_LEN}; pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/"; -pub const DEFAULT_DATA_DIR: &str = ".lighthouse/validators"; -pub const DEFAULT_SECRETS_DIR: &str = ".lighthouse/secrets"; /// Path to the slashing protection database within the datadir. pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; @@ -14,7 +16,7 @@ pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; #[derive(Clone, Serialize, Deserialize)] pub struct Config { /// The data directory, which stores all validator databases - pub data_dir: PathBuf, + pub validator_dir: PathBuf, /// The directory containing the passwords to unlock validator keystores. pub secrets_dir: PathBuf, /// The http endpoint of the beacon node API. @@ -28,6 +30,8 @@ pub struct Config { pub delete_lockfiles: bool, /// If true, don't scan the validators dir for new keystores. pub disable_auto_discover: bool, + /// If true, don't re-register existing validators in definitions.yml for slashing protection. + pub strict_slashing_protection: bool, /// Graffiti to be inserted everytime we create a block. pub graffiti: Option<Graffiti>, } @@ -35,19 +39,22 @@ pub struct Config { impl Default for Config { /// Build a new configuration from defaults. fn default() -> Self { - let data_dir = dirs::home_dir() - .map(|home| home.join(DEFAULT_DATA_DIR)) - .unwrap_or_else(|| PathBuf::from(".")); - let secrets_dir = dirs::home_dir() - .map(|home| home.join(DEFAULT_SECRETS_DIR)) - .unwrap_or_else(|| PathBuf::from(".")); + // WARNING: these directory defaults should be always overrided with parameters + // from cli for specific networks. + let base_dir = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(DEFAULT_ROOT_DIR) + .join(DEFAULT_HARDCODED_TESTNET); + let validator_dir = base_dir.join(DEFAULT_VALIDATOR_DIR); + let secrets_dir = base_dir.join(DEFAULT_SECRET_DIR); Self { - data_dir, + validator_dir, secrets_dir, http_server: DEFAULT_HTTP_SERVER.to_string(), allow_unsynced_beacon_node: false, delete_lockfiles: false, disable_auto_discover: false, + strict_slashing_protection: false, graffiti: None, } } @@ -59,16 +66,39 @@ impl Config { pub fn from_cli(cli_args: &ArgMatches) -> Result<Config, String> { let mut config = Config::default(); - config.data_dir = parse_path_with_default_in_home_dir( - cli_args, - "datadir", - PathBuf::from(".lighthouse").join("validators"), - )?; + let default_root_dir = dirs::home_dir() + .map(|home| home.join(DEFAULT_ROOT_DIR)) + .unwrap_or_else(|| PathBuf::from(".")); - if !config.data_dir.exists() { + let (mut validator_dir, mut secrets_dir) = (None, None); + if cli_args.value_of("datadir").is_some() { + let base_dir: PathBuf = parse_required(cli_args, "datadir")?; + validator_dir = Some(base_dir.join(DEFAULT_VALIDATOR_DIR)); + secrets_dir = Some(base_dir.join(DEFAULT_SECRET_DIR)); + } + if cli_args.value_of("validators-dir").is_some() + && cli_args.value_of("secrets-dir").is_some() + { + validator_dir = Some(parse_required(cli_args, "validators-dir")?); + secrets_dir = Some(parse_required(cli_args, "secrets-dir")?); + } + + config.validator_dir = validator_dir.unwrap_or_else(|| { + default_root_dir + .join(get_testnet_name(cli_args)) + .join(DEFAULT_VALIDATOR_DIR) + }); + + config.secrets_dir = secrets_dir.unwrap_or_else(|| { + default_root_dir + .join(get_testnet_name(cli_args)) + .join(DEFAULT_SECRET_DIR) + }); + + if !config.validator_dir.exists() { return Err(format!( - "The directory for validator data (--datadir) does not exist: {:?}", - config.data_dir + "The directory for validator data does not exist: {:?}", + config.validator_dir )); } @@ -79,10 +109,7 @@ impl Config { config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced"); config.delete_lockfiles = cli_args.is_present("delete-lockfiles"); config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); - - if let Some(secrets_dir) = parse_optional(cli_args, "secrets-dir")? { - config.secrets_dir = secrets_dir; - } + config.strict_slashing_protection = cli_args.is_present("strict-slashing-protection"); if let Some(input_graffiti) = cli_args.value_of("graffiti") { let graffiti_bytes = input_graffiti.as_bytes(); diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 6b709023f..6d82baa6b 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -68,18 +68,18 @@ impl<T: EthSpec> ProductionValidatorClient<T> { log, "Starting validator client"; "beacon_node" => &config.http_server, - "datadir" => format!("{:?}", config.data_dir), + "validator_dir" => format!("{:?}", config.validator_dir), ); - let mut validator_defs = ValidatorDefinitions::open_or_create(&config.data_dir) + let mut validator_defs = ValidatorDefinitions::open_or_create(&config.validator_dir) .map_err(|e| format!("Unable to open or create validator definitions: {:?}", e))?; if !config.disable_auto_discover { let new_validators = validator_defs - .discover_local_keystores(&config.data_dir, &config.secrets_dir, &log) + .discover_local_keystores(&config.validator_dir, &config.secrets_dir, &log) .map_err(|e| format!("Unable to discover local validator keystores: {:?}", e))?; validator_defs - .save(&config.data_dir) + .save(&config.validator_dir) .map_err(|e| format!("Unable to update validator definitions: {:?}", e))?; info!( log, @@ -90,7 +90,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { let validators = InitializedValidators::from_definitions( validator_defs, - config.data_dir.clone(), + config.validator_dir.clone(), config.delete_lockfiles, log.clone(), ) diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index f7d0442d3..66a616ff3 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -62,14 +62,24 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { fork_service: ForkService<T, E>, log: Logger, ) -> Result<Self, String> { - let slashing_db_path = config.data_dir.join(SLASHING_PROTECTION_FILENAME); - let slashing_protection = + let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = if config.strict_slashing_protection { + // Don't create a new slashing database if `strict_slashing_protection` is turned on. + SlashingDatabase::open(&slashing_db_path).map_err(|e| { + format!( + "Failed to open slashing protection database: {:?}. + Ensure that `slashing_protection.sqlite` is in {:?} folder", + e, config.validator_dir + ) + })? + } else { SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| { format!( "Failed to open or create slashing protection database: {:?}", e ) - })?; + })? + }; Ok(Self { validators: Arc::new(RwLock::new(validators)), From cdec3cec18d4af7d13a61243428e90e8b7529d31 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Tue, 29 Sep 2020 03:46:54 +0000 Subject: [PATCH 04/32] Implement standard eth2.0 API (#1569) - Resolves #1550 - Resolves #824 - Resolves #825 - Resolves #1131 - Resolves #1411 - Resolves #1256 - Resolve #1177 - Includes the `ShufflingId` struct initially defined in #1492. That PR is now closed and the changes are included here, with significant bug fixes. - Implement the https://github.com/ethereum/eth2.0-APIs in a new `http_api` crate using `warp`. This replaces the `rest_api` crate. - Add a new `common/eth2` crate which provides a wrapper around `reqwest`, providing the HTTP client that is used by the validator client and for testing. This replaces the `common/remote_beacon_node` crate. - Create a `http_metrics` crate which is a dedicated server for Prometheus metrics (they are no longer served on the same port as the REST API). We now have flags for `--metrics`, `--metrics-address`, etc. - Allow the `subnet_id` to be an optional parameter for `VerifiedUnaggregatedAttestation::verify`. This means it does not need to be provided unnecessarily by the validator client. - Move `fn map_attestation_committee` in `mod beacon_chain::attestation_verification` to a new `fn with_committee_cache` on the `BeaconChain` so the same cache can be used for obtaining validator duties. - Add some other helpers to `BeaconChain` to assist with common API duties (e.g., `block_root_at_slot`, `head_beacon_block_root`). - Change the `NaiveAggregationPool` so it can index attestations by `hash_tree_root(attestation.data)`. This is a requirement of the API. - Add functions to `BeaconChainHarness` to allow it to create slashings and exits. - Allow for `eth1::Eth1NetworkId` to go to/from a `String`. - Add functions to the `OperationPool` to allow getting all objects in the pool. - Add function to `BeaconState` to check if a committee cache is initialized. - Fix bug where `seconds_per_eth1_block` was not transferring over from `YamlConfig` to `ChainSpec`. - Add the `deposit_contract_address` to `YamlConfig` and `ChainSpec`. We needed to be able to return it in an API response. - Change some uses of serde `serialize_with` and `deserialize_with` to a single use of `with` (code quality). - Impl `Display` and `FromStr` for several BLS fields. - Check for clock discrepancy when VC polls BN for sync state (with +/- 1 slot tolerance). This is not intended to be comprehensive, it was just easy to do. - See #1434 for a per-endpoint overview. - Seeking clarity here: https://github.com/ethereum/eth2.0-APIs/issues/75 - [x] Add docs for prom port to close #1256 - [x] Follow up on this #1177 - [x] ~~Follow up with #1424~~ Will fix in future PR. - [x] Follow up with #1411 - [x] ~~Follow up with #1260~~ Will fix in future PR. - [x] Add quotes to all integers. - [x] Remove `rest_types` - [x] Address missing beacon block error. (#1629) - [x] ~~Add tests for lighthouse/peers endpoints~~ Wontfix - [x] ~~Follow up with validator status proposal~~ Tracked in #1434 - [x] Unify graffiti structs - [x] ~~Start server when waiting for genesis?~~ Will fix in future PR. - [x] TODO in http_api tests - [x] Move lighthouse endpoints off /eth/v1 - [x] Update docs to link to standard - ~~Blocked on #1586~~ Co-authored-by: Michael Sproul <michael@sigmaprime.io> --- Cargo.lock | 452 +++-- Cargo.toml | 8 +- beacon_node/beacon_chain/Cargo.toml | 1 - .../src/attestation_verification.rs | 163 +- beacon_node/beacon_chain/src/beacon_chain.rs | 282 ++- beacon_node/beacon_chain/src/builder.rs | 10 +- beacon_node/beacon_chain/src/errors.rs | 4 + .../src/naive_aggregation_pool.rs | 47 +- .../beacon_chain/src/shuffling_cache.rs | 38 +- beacon_node/beacon_chain/src/test_utils.rs | 114 +- .../tests/attestation_verification.rs | 6 +- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- beacon_node/beacon_chain/tests/tests.rs | 2 +- beacon_node/client/Cargo.toml | 3 +- beacon_node/client/src/builder.rs | 159 +- beacon_node/client/src/config.rs | 6 +- beacon_node/client/src/lib.rs | 16 +- beacon_node/eth1/src/http.rs | 33 +- beacon_node/eth1/src/lib.rs | 4 +- beacon_node/{rest_api => http_api}/Cargo.toml | 60 +- .../http_api/src/beacon_proposer_cache.rs | 185 ++ beacon_node/http_api/src/block_id.rs | 87 + beacon_node/http_api/src/lib.rs | 1749 ++++++++++++++++ beacon_node/http_api/src/metrics.rs | 32 + beacon_node/http_api/src/state_id.rs | 118 ++ .../http_api/src/validator_inclusion.rs | 88 + beacon_node/http_api/tests/tests.rs | 1786 +++++++++++++++++ beacon_node/http_metrics/Cargo.toml | 28 + beacon_node/http_metrics/src/lib.rs | 135 ++ .../{rest_api => http_metrics}/src/metrics.rs | 59 +- beacon_node/http_metrics/tests/tests.rs | 46 + beacon_node/network/Cargo.toml | 1 - .../network/src/attestation_service/mod.rs | 3 +- .../network/src/beacon_processor/worker.rs | 2 +- beacon_node/network/src/service.rs | 3 +- beacon_node/operation_pool/src/lib.rs | 45 + beacon_node/rest_api/src/beacon.rs | 499 ----- beacon_node/rest_api/src/config.rs | 55 - beacon_node/rest_api/src/consensus.rs | 126 -- beacon_node/rest_api/src/helpers.rs | 260 --- beacon_node/rest_api/src/lib.rs | 127 -- beacon_node/rest_api/src/lighthouse.rs | 48 - beacon_node/rest_api/src/node.rs | 39 - beacon_node/rest_api/src/router.rs | 322 --- beacon_node/rest_api/src/url_query.rs | 166 -- beacon_node/rest_api/src/validator.rs | 747 ------- beacon_node/rest_api/tests/test.rs | 1345 ------------- beacon_node/src/cli.rs | 34 +- beacon_node/src/config.rs | 53 +- beacon_node/src/lib.rs | 19 +- beacon_node/tests/test.rs | 9 +- book/src/SUMMARY.md | 19 +- book/src/advanced_metrics.md | 34 + book/src/api-bn.md | 130 ++ book/src/api-lighthouse.md | 179 ++ book/src/api-vc.md | 3 + book/src/api.md | 14 +- book/src/http.md | 26 +- book/src/http/advanced.md | 115 -- book/src/http/beacon.md | 784 -------- book/src/http/lighthouse.md | 182 -- book/src/http/network.md | 148 -- book/src/http/node.md | 91 - book/src/http/spec.md | 154 -- book/src/http/validator.md | 545 ----- .../consensus.md => validator-inclusion.md} | 135 +- book/src/websockets.md | 111 - common/eth2/Cargo.toml | 25 + common/eth2/src/lib.rs | 784 ++++++++ common/eth2/src/lighthouse.rs | 224 +++ common/eth2/src/types.rs | 432 ++++ common/lighthouse_metrics/src/lib.rs | 14 + common/remote_beacon_node/Cargo.toml | 21 - common/remote_beacon_node/src/lib.rs | 732 ------- common/rest_types/Cargo.toml | 27 - common/rest_types/src/api_error.rs | 99 - common/rest_types/src/beacon.rs | 65 - common/rest_types/src/consensus.rs | 66 - common/rest_types/src/handler.rs | 247 --- common/rest_types/src/lib.rs | 22 - common/rest_types/src/node.rs | 103 - common/rest_types/src/validator.rs | 103 - common/slot_clock/src/lib.rs | 10 + common/warp_utils/Cargo.toml | 15 + common/warp_utils/src/lib.rs | 5 + common/warp_utils/src/reject.rs | 168 ++ common/warp_utils/src/reply.rs | 15 + consensus/fork_choice/src/fork_choice.rs | 16 +- consensus/fork_choice/src/lib.rs | 1 + consensus/fork_choice/tests/tests.rs | 2 +- .../src/fork_choice_test_definition.rs | 13 +- consensus/proto_array/src/proto_array.rs | 6 +- .../src/proto_array_fork_choice.rs | 17 +- consensus/serde_hex/Cargo.toml | 9 - consensus/serde_utils/Cargo.toml | 1 + consensus/serde_utils/src/bytes_4_hex.rs | 38 + .../src/lib.rs => serde_utils/src/hex.rs} | 12 + consensus/serde_utils/src/lib.rs | 9 +- consensus/serde_utils/src/quoted_int.rs | 144 ++ consensus/serde_utils/src/quoted_u64.rs | 115 -- consensus/serde_utils/src/quoted_u64_vec.rs | 8 +- consensus/serde_utils/src/u32_hex.rs | 21 + consensus/serde_utils/src/u8_hex.rs | 29 + consensus/ssz_types/Cargo.toml | 2 +- consensus/ssz_types/src/bitfield.rs | 2 +- consensus/types/Cargo.toml | 2 + consensus/types/src/aggregate_and_proof.rs | 1 + consensus/types/src/attestation_data.rs | 1 + consensus/types/src/attestation_duty.rs | 3 + consensus/types/src/beacon_block.rs | 1 + consensus/types/src/beacon_block_body.rs | 5 - consensus/types/src/beacon_block_header.rs | 1 + consensus/types/src/beacon_state.rs | 9 + .../types/src/beacon_state/committee_cache.rs | 1 + consensus/types/src/chain_spec.rs | 138 +- consensus/types/src/deposit_data.rs | 1 + consensus/types/src/deposit_message.rs | 1 + consensus/types/src/enr_fork_id.rs | 11 +- consensus/types/src/eth1_data.rs | 1 + consensus/types/src/fork.rs | 11 +- consensus/types/src/fork_data.rs | 6 +- consensus/types/src/free_attestation.rs | 1 + consensus/types/src/graffiti.rs | 132 ++ consensus/types/src/indexed_attestation.rs | 38 + consensus/types/src/lib.rs | 8 +- consensus/types/src/pending_attestation.rs | 2 + consensus/types/src/shuffling_id.rs | 61 + consensus/types/src/slot_epoch_macros.rs | 13 + consensus/types/src/subnet_id.rs | 3 +- consensus/types/src/utils.rs | 3 - consensus/types/src/utils/serde_utils.rs | 134 -- consensus/types/src/validator_subscription.rs | 21 + consensus/types/src/voluntary_exit.rs | 1 + crypto/bls/Cargo.toml | 2 +- crypto/bls/src/generic_aggregate_signature.rs | 19 +- crypto/bls/src/generic_public_key.rs | 10 +- crypto/bls/src/generic_public_key_bytes.rs | 20 +- crypto/bls/src/generic_signature.rs | 10 +- crypto/bls/src/generic_signature_bytes.rs | 10 +- crypto/bls/src/macros.rs | 53 +- testing/node_test_rig/Cargo.toml | 2 +- testing/node_test_rig/src/lib.rs | 37 +- testing/simulator/src/checks.rs | 20 +- testing/simulator/src/cli.rs | 4 +- testing/simulator/src/local_network.rs | 15 +- testing/simulator/src/sync_sim.rs | 6 +- validator_client/Cargo.toml | 3 +- validator_client/src/attestation_service.rs | 332 ++- validator_client/src/block_service.rs | 47 +- validator_client/src/config.rs | 8 +- validator_client/src/duties_service.rs | 282 ++- validator_client/src/fork_service.rs | 22 +- .../src/initialized_validators.rs | 3 +- validator_client/src/is_synced.rs | 78 +- validator_client/src/lib.rs | 136 +- validator_client/src/validator_duty.rs | 131 ++ 156 files changed, 8862 insertions(+), 8916 deletions(-) rename beacon_node/{rest_api => http_api}/Cargo.toml (51%) create mode 100644 beacon_node/http_api/src/beacon_proposer_cache.rs create mode 100644 beacon_node/http_api/src/block_id.rs create mode 100644 beacon_node/http_api/src/lib.rs create mode 100644 beacon_node/http_api/src/metrics.rs create mode 100644 beacon_node/http_api/src/state_id.rs create mode 100644 beacon_node/http_api/src/validator_inclusion.rs create mode 100644 beacon_node/http_api/tests/tests.rs create mode 100644 beacon_node/http_metrics/Cargo.toml create mode 100644 beacon_node/http_metrics/src/lib.rs rename beacon_node/{rest_api => http_metrics}/src/metrics.rs (69%) create mode 100644 beacon_node/http_metrics/tests/tests.rs delete mode 100644 beacon_node/rest_api/src/beacon.rs delete mode 100644 beacon_node/rest_api/src/config.rs delete mode 100644 beacon_node/rest_api/src/consensus.rs delete mode 100644 beacon_node/rest_api/src/helpers.rs delete mode 100644 beacon_node/rest_api/src/lib.rs delete mode 100644 beacon_node/rest_api/src/lighthouse.rs delete mode 100644 beacon_node/rest_api/src/node.rs delete mode 100644 beacon_node/rest_api/src/router.rs delete mode 100644 beacon_node/rest_api/src/url_query.rs delete mode 100644 beacon_node/rest_api/src/validator.rs delete mode 100644 beacon_node/rest_api/tests/test.rs create mode 100644 book/src/advanced_metrics.md create mode 100644 book/src/api-bn.md create mode 100644 book/src/api-lighthouse.md create mode 100644 book/src/api-vc.md delete mode 100644 book/src/http/advanced.md delete mode 100644 book/src/http/beacon.md delete mode 100644 book/src/http/lighthouse.md delete mode 100644 book/src/http/network.md delete mode 100644 book/src/http/node.md delete mode 100644 book/src/http/spec.md delete mode 100644 book/src/http/validator.md rename book/src/{http/consensus.md => validator-inclusion.md} (52%) delete mode 100644 book/src/websockets.md create mode 100644 common/eth2/Cargo.toml create mode 100644 common/eth2/src/lib.rs create mode 100644 common/eth2/src/lighthouse.rs create mode 100644 common/eth2/src/types.rs delete mode 100644 common/remote_beacon_node/Cargo.toml delete mode 100644 common/remote_beacon_node/src/lib.rs delete mode 100644 common/rest_types/Cargo.toml delete mode 100644 common/rest_types/src/api_error.rs delete mode 100644 common/rest_types/src/beacon.rs delete mode 100644 common/rest_types/src/consensus.rs delete mode 100644 common/rest_types/src/handler.rs delete mode 100644 common/rest_types/src/lib.rs delete mode 100644 common/rest_types/src/node.rs delete mode 100644 common/rest_types/src/validator.rs create mode 100644 common/warp_utils/Cargo.toml create mode 100644 common/warp_utils/src/lib.rs create mode 100644 common/warp_utils/src/reject.rs create mode 100644 common/warp_utils/src/reply.rs delete mode 100644 consensus/serde_hex/Cargo.toml create mode 100644 consensus/serde_utils/src/bytes_4_hex.rs rename consensus/{serde_hex/src/lib.rs => serde_utils/src/hex.rs} (81%) create mode 100644 consensus/serde_utils/src/quoted_int.rs delete mode 100644 consensus/serde_utils/src/quoted_u64.rs create mode 100644 consensus/serde_utils/src/u32_hex.rs create mode 100644 consensus/serde_utils/src/u8_hex.rs create mode 100644 consensus/types/src/graffiti.rs create mode 100644 consensus/types/src/shuffling_id.rs delete mode 100644 consensus/types/src/utils.rs delete mode 100644 consensus/types/src/utils/serde_utils.rs create mode 100644 consensus/types/src/validator_subscription.rs create mode 100644 validator_client/src/validator_duty.rs diff --git a/Cargo.lock b/Cargo.lock index 73c7d707a..a94d97af3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,12 +227,6 @@ dependencies = [ "syn", ] -[[package]] -name = "assert_matches" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7deb0a829ca7bcfaf5da70b073a8d128619259a7be8216a355e23f00763059e5" - [[package]] name = "async-tls" version = "0.8.0" @@ -349,7 +343,6 @@ dependencies = [ "rand 0.7.3", "rand_core 0.5.1", "rayon", - "regex", "safe_arith", "serde", "serde_derive", @@ -519,7 +512,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_derive", - "serde_hex", + "serde_utils", "tree_hash", "zeroize", ] @@ -575,6 +568,16 @@ dependencies = [ "serde", ] +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + [[package]] name = "bumpalo" version = "3.4.0" @@ -770,13 +773,14 @@ dependencies = [ "eth2_ssz", "futures 0.3.5", "genesis", + "http_api", + "http_metrics", "lazy_static", "lighthouse_metrics", "network", "parking_lot 0.11.0", "prometheus", "reqwest", - "rest_api", "serde", "serde_derive", "serde_yaml", @@ -1460,6 +1464,22 @@ dependencies = [ "web3", ] +[[package]] +name = "eth2" +version = "0.1.0" +dependencies = [ + "eth2_libp2p", + "hex 0.4.2", + "procinfo", + "proto_array", + "psutil", + "reqwest", + "serde", + "serde_json", + "serde_utils", + "types", +] + [[package]] name = "eth2_config" version = "0.2.0" @@ -1600,7 +1620,7 @@ dependencies = [ "eth2_ssz", "serde", "serde_derive", - "serde_hex", + "serde_utils", "tree_hash", "tree_hash_derive", "typenum", @@ -2148,6 +2168,31 @@ dependencies = [ "tokio 0.2.22", ] +[[package]] +name = "headers" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed18eb2459bf1a09ad2d6b1547840c3e5e62882fa09b9a6a20b1de8e3228848f" +dependencies = [ + "base64 0.12.3", + "bitflags 1.2.1", + "bytes 0.5.6", + "headers-core", + "http 0.2.1", + "mime 0.3.16", + "sha-1 0.8.2", + "time 0.1.44", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.1", +] + [[package]] name = "heck" version = "0.3.1" @@ -2269,6 +2314,58 @@ dependencies = [ "http 0.2.1", ] +[[package]] +name = "http_api" +version = "0.1.0" +dependencies = [ + "beacon_chain", + "discv5", + "environment", + "eth1", + "eth2", + "eth2_libp2p", + "fork_choice", + "hex 0.4.2", + "lazy_static", + "lighthouse_metrics", + "lighthouse_version", + "network", + "parking_lot 0.11.0", + "serde", + "slog", + "slot_clock", + "state_processing", + "store", + "tokio 0.2.22", + "tree_hash", + "types", + "warp", + "warp_utils", +] + +[[package]] +name = "http_metrics" +version = "0.1.0" +dependencies = [ + "beacon_chain", + "environment", + "eth2", + "eth2_libp2p", + "lazy_static", + "lighthouse_metrics", + "lighthouse_version", + "prometheus", + "reqwest", + "serde", + "slog", + "slot_clock", + "store", + "tokio 0.2.22", + "types", + "warp", + "warp_utils", +] + [[package]] name = "httparse" version = "1.3.4" @@ -2448,6 +2545,15 @@ dependencies = [ "hashbrown 0.9.1", ] +[[package]] +name = "input_buffer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" +dependencies = [ + "bytes 0.5.6", +] + [[package]] name = "instant" version = "0.1.7" @@ -3259,6 +3365,24 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1255076139a83bb467426e7f8d0134968a8118844faa755985e077cf31850333" +[[package]] +name = "multipart" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8209c33c951f07387a8497841122fc6f712165e3f9bda3e6be4645b58188f676" +dependencies = [ + "buf_redux", + "httparse", + "log 0.4.11", + "mime 0.3.16", + "mime_guess", + "quick-error", + "rand 0.6.5", + "safemem", + "tempfile", + "twoway", +] + [[package]] name = "multistream-select" version = "0.8.2" @@ -3339,7 +3463,6 @@ dependencies = [ "num_cpus", "parking_lot 0.11.0", "rand 0.7.3", - "rest_types", "rlp", "slog", "sloggers", @@ -3372,10 +3495,10 @@ version = "0.2.0" dependencies = [ "beacon_node", "environment", + "eth2", "eth2_config", "futures 0.3.5", "genesis", - "remote_beacon_node", "reqwest", "serde", "tempdir", @@ -4054,6 +4177,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift 0.1.1", + "winapi 0.3.9", +] + [[package]] name = "rand" version = "0.7.3" @@ -4062,9 +4204,19 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", ] [[package]] @@ -4101,6 +4253,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -4110,6 +4271,59 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi 0.0.3", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_xorshift" version = "0.2.0" @@ -4197,24 +4411,6 @@ version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" -[[package]] -name = "remote_beacon_node" -version = "0.2.0" -dependencies = [ - "eth2_config", - "eth2_ssz", - "futures 0.3.5", - "hex 0.4.2", - "operation_pool", - "proto_array", - "reqwest", - "rest_types", - "serde", - "serde_json", - "types", - "url 2.1.1", -] - [[package]] name = "remove_dir_all" version = "0.5.3" @@ -4260,73 +4456,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "rest_api" -version = "0.2.0" -dependencies = [ - "assert_matches", - "beacon_chain", - "bls", - "bus", - "environment", - "eth2_config", - "eth2_libp2p", - "eth2_ssz", - "eth2_ssz_derive", - "futures 0.3.5", - "hex 0.4.2", - "http 0.2.1", - "hyper 0.13.8", - "itertools 0.9.0", - "lazy_static", - "lighthouse_metrics", - "lighthouse_version", - "network", - "node_test_rig", - "operation_pool", - "parking_lot 0.11.0", - "remote_beacon_node", - "rest_types", - "serde", - "serde_json", - "serde_yaml", - "slog", - "slog-async", - "slog-term", - "slot_clock", - "state_processing", - "store", - "tokio 0.2.22", - "tree_hash", - "types", - "uhttp_sse", - "url 2.1.1", -] - -[[package]] -name = "rest_types" -version = "0.2.0" -dependencies = [ - "beacon_chain", - "bls", - "environment", - "eth2_hashing", - "eth2_ssz", - "eth2_ssz_derive", - "hyper 0.13.8", - "procinfo", - "psutil", - "rayon", - "serde", - "serde_json", - "serde_yaml", - "state_processing", - "store", - "tokio 0.2.22", - "tree_hash", - "types", -] - [[package]] name = "ring" version = "0.16.12" @@ -4615,14 +4744,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_hex" -version = "0.2.0" -dependencies = [ - "hex 0.4.2", - "serde", -] - [[package]] name = "serde_json" version = "1.0.57" @@ -4661,6 +4782,7 @@ dependencies = [ name = "serde_utils" version = "0.1.0" dependencies = [ + "hex 0.4.2", "serde", "serde_derive", "serde_json", @@ -5668,6 +5790,19 @@ dependencies = [ "tokio 0.2.22", ] +[[package]] +name = "tokio-tungstenite" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e878ad426ca286e4dcae09cbd4e1973a7f8987d97570e2469703dd7f5720c" +dependencies = [ + "futures-util", + "log 0.4.11", + "pin-project", + "tokio 0.2.22", + "tungstenite", +] + [[package]] name = "tokio-udp" version = "0.1.6" @@ -5769,6 +5904,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "trackable" version = "1.0.0" @@ -5822,6 +5967,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tungstenite" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0308d80d86700c5878b9ef6321f020f29b1bb9d5ff3cab25e75e23f3a492a23" +dependencies = [ + "base64 0.12.3", + "byteorder", + "bytes 0.5.6", + "http 0.2.1", + "httparse", + "input_buffer", + "log 0.4.11", + "rand 0.7.3", + "sha-1 0.9.1", + "url 2.1.1", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + [[package]] name = "typeable" version = "0.1.2" @@ -5856,13 +6029,15 @@ dependencies = [ "log 0.4.11", "merkle_proof", "rand 0.7.3", - "rand_xorshift", + "rand_xorshift 0.2.0", "rayon", + "regex", "rusqlite", "safe_arith", "serde", "serde_derive", "serde_json", + "serde_utils", "serde_yaml", "slog", "swap_or_not_shuffle", @@ -5872,12 +6047,6 @@ dependencies = [ "tree_hash_derive", ] -[[package]] -name = "uhttp_sse" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ff93345ba2206230b1bb1aa3ece1a63dd9443b7531024575d16a0680a59444" - [[package]] name = "uint" version = "0.8.5" @@ -6018,6 +6187,18 @@ dependencies = [ "percent-encoding 2.1.0", ] +[[package]] +name = "urlencoding" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9232eb53352b4442e40d7900465dfc534e8cb2dc8f18656fcb2ac16112b5593" + +[[package]] +name = "utf-8" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" + [[package]] name = "uuid" version = "0.8.1" @@ -6040,6 +6221,7 @@ dependencies = [ "directory", "dirs", "environment", + "eth2", "eth2_config", "eth2_interop_keypairs", "eth2_keystore", @@ -6052,8 +6234,6 @@ dependencies = [ "logging", "parking_lot 0.11.0", "rayon", - "remote_beacon_node", - "rest_types", "serde", "serde_derive", "serde_json", @@ -6148,6 +6328,46 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41be6df54c97904af01aa23e613d4521eed7ab23537cede692d4058f6449407" +dependencies = [ + "bytes 0.5.6", + "futures 0.3.5", + "headers", + "http 0.2.1", + "hyper 0.13.8", + "log 0.4.11", + "mime 0.3.16", + "mime_guess", + "multipart", + "pin-project", + "scoped-tls 1.0.0", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 0.2.22", + "tokio-tungstenite", + "tower-service", + "tracing", + "tracing-futures", + "urlencoding", +] + +[[package]] +name = "warp_utils" +version = "0.1.0" +dependencies = [ + "beacon_chain", + "eth2", + "safe_arith", + "state_processing", + "types", + "warp", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 82922f5a5..b8b2fdde7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,9 @@ members = [ "beacon_node/client", "beacon_node/eth1", "beacon_node/eth2_libp2p", + "beacon_node/http_api", + "beacon_node/http_metrics", "beacon_node/network", - "beacon_node/rest_api", "beacon_node/store", "beacon_node/timer", "beacon_node/websocket_server", @@ -21,6 +22,7 @@ members = [ "common/compare_fields_derive", "common/deposit_contract", "common/directory", + "common/eth2", "common/eth2_config", "common/eth2_interop_keypairs", "common/eth2_testnet_config", @@ -30,10 +32,9 @@ members = [ "common/lighthouse_version", "common/logging", "common/lru_cache", - "common/remote_beacon_node", - "common/rest_types", "common/slot_clock", "common/test_random_derive", + "common/warp_utils", "common/validator_dir", "consensus/cached_tree_hash", @@ -44,7 +45,6 @@ members = [ "consensus/ssz", "consensus/ssz_derive", "consensus/ssz_types", - "consensus/serde_hex", "consensus/serde_utils", "consensus/state_processing", "consensus/swap_or_not_shuffle", diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 05ae819c4..04e22f426 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -58,4 +58,3 @@ environment = { path = "../../lighthouse/environment" } bus = "2.2.3" derivative = "2.1.1" itertools = "0.9.0" -regex = "1.3.9" diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 648033044..32a085902 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -28,8 +28,7 @@ use crate::{ beacon_chain::{ - ATTESTATION_CACHE_LOCK_TIMEOUT, HEAD_LOCK_TIMEOUT, MAXIMUM_GOSSIP_CLOCK_DISPARITY, - VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, + HEAD_LOCK_TIMEOUT, MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, }, metrics, observed_attestations::ObserveOutcome, @@ -38,12 +37,10 @@ use crate::{ }; use bls::verify_signature_sets; use proto_array::Block as ProtoBlock; -use slog::debug; use slot_clock::SlotClock; use state_processing::{ common::get_indexed_attestation, per_block_processing::errors::AttestationValidationError, - per_slot_processing, signature_sets::{ indexed_attestation_signature_set_from_pubkeys, signed_aggregate_selection_proof_signature_set, signed_aggregate_signature_set, @@ -53,7 +50,7 @@ use std::borrow::Cow; use tree_hash::TreeHash; use types::{ Attestation, BeaconCommittee, CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, - RelativeEpoch, SelectionProof, SignedAggregateAndProof, Slot, SubnetId, + SelectionProof, SignedAggregateAndProof, Slot, SubnetId, }; /// Returned when an attestation was not successfully verified. It might not have been verified for @@ -267,6 +264,7 @@ pub struct VerifiedAggregatedAttestation<T: BeaconChainTypes> { pub struct VerifiedUnaggregatedAttestation<T: BeaconChainTypes> { attestation: Attestation<T::EthSpec>, indexed_attestation: IndexedAttestation<T::EthSpec>, + subnet_id: SubnetId, } /// Custom `Clone` implementation is to avoid the restrictive trait bounds applied by the usual derive @@ -276,6 +274,7 @@ impl<T: BeaconChainTypes> Clone for VerifiedUnaggregatedAttestation<T> { Self { attestation: self.attestation.clone(), indexed_attestation: self.indexed_attestation.clone(), + subnet_id: self.subnet_id, } } } @@ -428,6 +427,11 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> { pub fn attestation(&self) -> &Attestation<T::EthSpec> { &self.signed_aggregate.message.aggregate } + + /// Returns the underlying `signed_aggregate`. + pub fn aggregate(&self) -> &SignedAggregateAndProof<T::EthSpec> { + &self.signed_aggregate + } } impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { @@ -438,7 +442,7 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { /// verify that it was received on the correct subnet. pub fn verify( attestation: Attestation<T::EthSpec>, - subnet_id: SubnetId, + subnet_id: Option<SubnetId>, chain: &BeaconChain<T>, ) -> Result<Self, Error> { let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); @@ -513,13 +517,15 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { ) .map_err(BeaconChainError::from)?; - // Ensure the attestation is from the correct subnet. - if subnet_id != expected_subnet_id { - return Err(Error::InvalidSubnetId { - received: subnet_id, - expected: expected_subnet_id, - }); - } + // If a subnet was specified, ensure that subnet is correct. + if let Some(subnet_id) = subnet_id { + if subnet_id != expected_subnet_id { + return Err(Error::InvalidSubnetId { + received: subnet_id, + expected: expected_subnet_id, + }); + } + }; let validator_index = *indexed_attestation .attesting_indices @@ -564,6 +570,7 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { Ok(Self { attestation, indexed_attestation, + subnet_id: expected_subnet_id, }) } @@ -572,6 +579,11 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { chain.add_to_naive_aggregation_pool(self) } + /// Returns the correct subnet for the attestation. + pub fn subnet_id(&self) -> SubnetId { + self.subnet_id + } + /// Returns the wrapped `attestation`. pub fn attestation(&self) -> &Attestation<T::EthSpec> { &self.attestation @@ -587,6 +599,7 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { } /// Returns `Ok(())` if the `attestation.data.beacon_block_root` is known to this chain. +/// You can use this `shuffling_id` to read from the shuffling cache. /// /// The block root may not be known for two reasons: /// @@ -615,6 +628,7 @@ fn verify_head_block_is_known<T: BeaconChainTypes>( }); } } + Ok(block) } else { Err(Error::UnknownHeadBlock { @@ -770,7 +784,7 @@ type CommitteesPerSlot = u64; /// Returns the `indexed_attestation` and committee count per slot for the `attestation` using the /// public keys cached in the `chain`. -pub fn obtain_indexed_attestation_and_committees_per_slot<T: BeaconChainTypes>( +fn obtain_indexed_attestation_and_committees_per_slot<T: BeaconChainTypes>( chain: &BeaconChain<T>, attestation: &Attestation<T::EthSpec>, ) -> Result<(IndexedAttestation<T::EthSpec>, CommitteesPerSlot), Error> { @@ -790,8 +804,8 @@ pub fn obtain_indexed_attestation_and_committees_per_slot<T: BeaconChainTypes>( /// /// If the committee for `attestation` isn't found in the `shuffling_cache`, we will read a state /// from disk and then update the `shuffling_cache`. -pub fn map_attestation_committee<'a, T, F, R>( - chain: &'a BeaconChain<T>, +fn map_attestation_committee<T, F, R>( + chain: &BeaconChain<T>, attestation: &Attestation<T::EthSpec>, map_fn: F, ) -> Result<R, Error> @@ -809,104 +823,23 @@ where // processing an attestation that does not include our latest finalized block in its chain. // // We do not delay consideration for later, we simply drop the attestation. - let target_block = chain - .fork_choice - .read() - .get_block(&target.root) - .ok_or_else(|| Error::UnknownTargetRoot(target.root))?; - - // Obtain the shuffling cache, timing how long we wait. - let cache_wait_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); - - let mut shuffling_cache = chain - .shuffling_cache - .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) - .ok_or_else(|| BeaconChainError::AttestationCacheLockTimeout)?; - - metrics::stop_timer(cache_wait_timer); - - if let Some(committee_cache) = shuffling_cache.get(attestation_epoch, target.root) { - let committees_per_slot = committee_cache.committees_per_slot(); - committee_cache - .get_beacon_committee(attestation.data.slot, attestation.data.index) - .map(|committee| map_fn((committee, committees_per_slot))) - .unwrap_or_else(|| { - Err(Error::NoCommitteeForSlotAndIndex { - slot: attestation.data.slot, - index: attestation.data.index, - }) - }) - } else { - // Drop the shuffling cache to avoid holding the lock for any longer than - // required. - drop(shuffling_cache); - - debug!( - chain.log, - "Attestation processing cache miss"; - "attn_epoch" => attestation_epoch.as_u64(), - "target_block_epoch" => target_block.slot.epoch(T::EthSpec::slots_per_epoch()).as_u64(), - ); - - let state_read_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); - - let mut state = chain - .store - .get_inconsistent_state_for_attestation_verification_only( - &target_block.state_root, - Some(target_block.slot), - ) - .map_err(BeaconChainError::from)? - .ok_or_else(|| BeaconChainError::MissingBeaconState(target_block.state_root))?; - - metrics::stop_timer(state_read_timer); - let state_skip_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); - - while state.current_epoch() + 1 < attestation_epoch { - // Here we tell `per_slot_processing` to skip hashing the state and just - // use the zero hash instead. - // - // The state roots are not useful for the shuffling, so there's no need to - // compute them. - per_slot_processing(&mut state, Some(Hash256::zero()), &chain.spec) - .map_err(BeaconChainError::from)?; - } - - metrics::stop_timer(state_skip_timer); - let committee_building_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); - - let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), attestation_epoch) - .map_err(BeaconChainError::IncorrectStateForAttestation)?; - - state - .build_committee_cache(relative_epoch, &chain.spec) - .map_err(BeaconChainError::from)?; - - let committee_cache = state - .committee_cache(relative_epoch) - .map_err(BeaconChainError::from)?; - - chain - .shuffling_cache - .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) - .ok_or_else(|| BeaconChainError::AttestationCacheLockTimeout)? - .insert(attestation_epoch, target.root, committee_cache); - - metrics::stop_timer(committee_building_timer); - - let committees_per_slot = committee_cache.committees_per_slot(); - committee_cache - .get_beacon_committee(attestation.data.slot, attestation.data.index) - .map(|committee| map_fn((committee, committees_per_slot))) - .unwrap_or_else(|| { - Err(Error::NoCommitteeForSlotAndIndex { - slot: attestation.data.slot, - index: attestation.data.index, - }) - }) + if !chain.fork_choice.read().contains_block(&target.root) { + return Err(Error::UnknownTargetRoot(target.root)); } + + chain + .with_committee_cache(target.root, attestation_epoch, |committee_cache| { + let committees_per_slot = committee_cache.committees_per_slot(); + + Ok(committee_cache + .get_beacon_committee(attestation.data.slot, attestation.data.index) + .map(|committee| map_fn((committee, committees_per_slot))) + .unwrap_or_else(|| { + Err(Error::NoCommitteeForSlotAndIndex { + slot: attestation.data.slot, + index: attestation.data.index, + }) + })) + }) + .map_err(BeaconChainError::from)? } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1caaec5fe..3bf5ae282 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -21,7 +21,7 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_fork_choice::PersistedForkChoice; -use crate::shuffling_cache::ShufflingCache; +use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::snapshot_cache::SnapshotCache; use crate::timeout_rw_lock::TimeoutRwLock; use crate::validator_pubkey_cache::ValidatorPubkeyCache; @@ -31,7 +31,6 @@ use fork_choice::ForkChoice; use itertools::process_results; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::RwLock; -use regex::bytes::Regex; use slog::{crit, debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; use state_processing::{ @@ -201,6 +200,8 @@ pub struct BeaconChain<T: BeaconChainTypes> { pub(crate) canonical_head: TimeoutRwLock<BeaconSnapshot<T::EthSpec>>, /// The root of the genesis block. pub genesis_block_root: Hash256, + /// The root of the genesis state. + pub genesis_state_root: Hash256, /// The root of the list of genesis validators, used during syncing. pub genesis_validators_root: Hash256, @@ -459,6 +460,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } } + /// Returns the block at the given slot, if any. Only returns blocks in the canonical chain. + /// + /// ## Errors + /// + /// May return a database error. + pub fn state_root_at_slot(&self, slot: Slot) -> Result<Option<Hash256>, Error> { + process_results(self.rev_iter_state_roots()?, |mut iter| { + iter.find(|(_, this_slot)| *this_slot == slot) + .map(|(root, _)| root) + }) + } + + /// Returns the block root at the given slot, if any. Only returns roots in the canonical chain. + /// + /// ## Errors + /// + /// May return a database error. + pub fn block_root_at_slot(&self, slot: Slot) -> Result<Option<Hash256>, Error> { + process_results(self.rev_iter_block_roots()?, |mut iter| { + iter.find(|(_, this_slot)| *this_slot == slot) + .map(|(root, _)| root) + }) + } + /// Returns the block at the given root, if any. /// /// ## Errors @@ -506,6 +531,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> { f(&head_lock) } + /// Returns the beacon block root at the head of the canonical chain. + /// + /// See `Self::head` for more information. + pub fn head_beacon_block_root(&self) -> Result<Hash256, Error> { + self.with_head(|s| Ok(s.beacon_block_root)) + } + + /// Returns the beacon block at the head of the canonical chain. + /// + /// See `Self::head` for more information. + pub fn head_beacon_block(&self) -> Result<SignedBeaconBlock<T::EthSpec>, Error> { + self.with_head(|s| Ok(s.beacon_block.clone())) + } + + /// Returns the beacon state at the head of the canonical chain. + /// + /// See `Self::head` for more information. + pub fn head_beacon_state(&self) -> Result<BeaconState<T::EthSpec>, Error> { + self.with_head(|s| { + Ok(s.beacon_state + .clone_with(CloneConfig::committee_caches_only())) + }) + } + /// Returns info representing the head block and state. /// /// A summarized version of `Self::head` that involves less cloning. @@ -719,46 +768,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .map_err(Into::into) } - /// Returns the attestation slot and committee index for a given validator index. + /// Returns the attestation duties for a given validator index. /// /// Information is read from the current state, so only information from the present and prior /// epoch is available. - pub fn validator_attestation_slot_and_index( + pub fn validator_attestation_duty( &self, validator_index: usize, epoch: Epoch, - ) -> Result<Option<(Slot, u64)>, Error> { - let as_epoch = |slot: Slot| slot.epoch(T::EthSpec::slots_per_epoch()); - let head_state = &self.head()?.beacon_state; + ) -> Result<Option<AttestationDuty>, Error> { + let head_block_root = self.head_beacon_block_root()?; - let mut state = if epoch == as_epoch(head_state.slot) { - self.head()?.beacon_state - } else { - // The block proposer shuffling is not affected by the state roots, so we don't need to - // calculate them. - self.state_at_slot( - epoch.start_slot(T::EthSpec::slots_per_epoch()), - StateSkipConfig::WithoutStateRoots, - )? - }; - - state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; - - if as_epoch(state.slot) != epoch { - return Err(Error::InvariantViolated(format!( - "Epochs in consistent in attestation duties lookup: state: {}, requested: {}", - as_epoch(state.slot), - epoch - ))); - } - - if let Some(attestation_duty) = - state.get_attestation_duties(validator_index, RelativeEpoch::Current)? - { - Ok(Some((attestation_duty.slot, attestation_duty.index))) - } else { - Ok(None) - } + self.with_committee_cache(head_block_root, epoch, |committee_cache| { + Ok(committee_cache.get_attestation_duties(validator_index)) + }) } /// Returns an aggregated `Attestation`, if any, that has a matching `attestation.data`. @@ -767,11 +790,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> { pub fn get_aggregated_attestation( &self, data: &AttestationData, - ) -> Result<Option<Attestation<T::EthSpec>>, Error> { + ) -> Option<Attestation<T::EthSpec>> { + self.naive_aggregation_pool.read().get(data) + } + + /// Returns an aggregated `Attestation`, if any, that has a matching + /// `attestation.data.tree_hash_root()`. + /// + /// The attestation will be obtained from `self.naive_aggregation_pool`. + pub fn get_aggregated_attestation_by_slot_and_root( + &self, + slot: Slot, + attestation_data_root: &Hash256, + ) -> Option<Attestation<T::EthSpec>> { self.naive_aggregation_pool .read() - .get(data) - .map_err(Into::into) + .get_by_slot_and_root(slot, attestation_data_root) } /// Produce an unaggregated `Attestation` that is valid for the given `slot` and `index`. @@ -898,7 +932,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { pub fn verify_unaggregated_attestation_for_gossip( &self, attestation: Attestation<T::EthSpec>, - subnet_id: SubnetId, + subnet_id: Option<SubnetId>, ) -> Result<VerifiedUnaggregatedAttestation<T>, AttestationError> { metrics::inc_counter(&metrics::UNAGGREGATED_ATTESTATION_PROCESSING_REQUESTS); let _timer = @@ -1320,11 +1354,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { block: SignedBeaconBlock<T::EthSpec>, ) -> Result<GossipVerifiedBlock<T>, BlockError<T::EthSpec>> { let slot = block.message.slot; - #[allow(clippy::invalid_regex)] - let re = Regex::new("\\p{C}").expect("regex is valid"); - let graffiti_string = - String::from_utf8_lossy(&re.replace_all(&block.message.body.graffiti[..], &b""[..])) - .to_string(); + let graffiti_string = block.message.body.graffiti.as_utf8_lossy(); match GossipVerifiedBlock::new(block, self) { Ok(verified) => { @@ -1449,8 +1479,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { ) -> Result<Hash256, BlockError<T::EthSpec>> { let signed_block = fully_verified_block.block; let block_root = fully_verified_block.block_root; - let state = fully_verified_block.state; - let parent_block = fully_verified_block.parent_block; + let mut state = fully_verified_block.state; let current_slot = self.slot()?; let mut ops = fully_verified_block.intermediate_states; @@ -1482,29 +1511,25 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .ok_or_else(|| Error::ValidatorPubkeyCacheLockTimeout)? .import_new_pubkeys(&state)?; - // If the imported block is in the previous or current epochs (according to the - // wall-clock), check to see if this is the first block of the epoch. If so, add the - // committee to the shuffling cache. - if state.current_epoch() + 1 >= self.epoch()? - && parent_block.slot().epoch(T::EthSpec::slots_per_epoch()) != state.current_epoch() - { - let mut shuffling_cache = self + // For the current and next epoch of this state, ensure we have the shuffling from this + // block in our cache. + for relative_epoch in &[RelativeEpoch::Current, RelativeEpoch::Next] { + let shuffling_id = ShufflingId::new(block_root, &state, *relative_epoch)?; + + let shuffling_is_cached = self .shuffling_cache - .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) - .ok_or_else(|| Error::AttestationCacheLockTimeout)?; + .try_read_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| Error::AttestationCacheLockTimeout)? + .contains(&shuffling_id); - let committee_cache = state.committee_cache(RelativeEpoch::Current)?; - - let epoch_start_slot = state - .current_epoch() - .start_slot(T::EthSpec::slots_per_epoch()); - let target_root = if state.slot == epoch_start_slot { - block_root - } else { - *state.get_block_root(epoch_start_slot)? - }; - - shuffling_cache.insert(state.current_epoch(), target_root, committee_cache); + if !shuffling_is_cached { + state.build_committee_cache(*relative_epoch, &self.spec)?; + let committee_cache = state.committee_cache(*relative_epoch)?; + self.shuffling_cache + .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| Error::AttestationCacheLockTimeout)? + .insert(shuffling_id, committee_cache); + } } let mut fork_choice = self.fork_choice.write(); @@ -1992,6 +2017,129 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(()) } + /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head + /// `head_block_root`. + /// + /// It's not necessary that `head_block_root` matches our current view of the chain, it can be + /// any block that is: + /// + /// - Known to us. + /// - The finalized block or a descendant of the finalized block. + /// + /// It would be quite common for attestation verification operations to use a `head_block_root` + /// that differs from our view of the head. + /// + /// ## Important + /// + /// This function is **not** suitable for determining proposer duties. + /// + /// ## Notes + /// + /// This function exists in this odd "map" pattern because efficiently obtaining a committee + /// can be complex. It might involve reading straight from the `beacon_chain.shuffling_cache` + /// or it might involve reading it from a state from the DB. Due to the complexities of + /// `RwLock`s on the shuffling cache, a simple `Cow` isn't suitable here. + /// + /// If the committee for `(head_block_root, shuffling_epoch)` isn't found in the + /// `shuffling_cache`, we will read a state from disk and then update the `shuffling_cache`. + pub(crate) fn with_committee_cache<F, R>( + &self, + head_block_root: Hash256, + shuffling_epoch: Epoch, + map_fn: F, + ) -> Result<R, Error> + where + F: Fn(&CommitteeCache) -> Result<R, Error>, + { + let head_block = self + .fork_choice + .read() + .get_block(&head_block_root) + .ok_or_else(|| Error::MissingBeaconBlock(head_block_root))?; + + let shuffling_id = BlockShufflingIds { + current: head_block.current_epoch_shuffling_id.clone(), + next: head_block.next_epoch_shuffling_id.clone(), + block_root: head_block.root, + } + .id_for_epoch(shuffling_epoch) + .ok_or_else(|| Error::InvalidShufflingId { + shuffling_epoch, + head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()), + })?; + + // Obtain the shuffling cache, timing how long we wait. + let cache_wait_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); + + let mut shuffling_cache = self + .shuffling_cache + .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| Error::AttestationCacheLockTimeout)?; + + metrics::stop_timer(cache_wait_timer); + + if let Some(committee_cache) = shuffling_cache.get(&shuffling_id) { + map_fn(committee_cache) + } else { + // Drop the shuffling cache to avoid holding the lock for any longer than + // required. + drop(shuffling_cache); + + debug!( + self.log, + "Committee cache miss"; + "shuffling_epoch" => shuffling_epoch.as_u64(), + "head_block_root" => head_block_root.to_string(), + ); + + let state_read_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); + + let mut state = self + .store + .get_inconsistent_state_for_attestation_verification_only( + &head_block.state_root, + Some(head_block.slot), + )? + .ok_or_else(|| Error::MissingBeaconState(head_block.state_root))?; + + metrics::stop_timer(state_read_timer); + let state_skip_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); + + while state.current_epoch() + 1 < shuffling_epoch { + // Here we tell `per_slot_processing` to skip hashing the state and just + // use the zero hash instead. + // + // The state roots are not useful for the shuffling, so there's no need to + // compute them. + per_slot_processing(&mut state, Some(Hash256::zero()), &self.spec) + .map_err(Error::from)?; + } + + metrics::stop_timer(state_skip_timer); + let committee_building_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); + + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), shuffling_epoch) + .map_err(Error::IncorrectStateForAttestation)?; + + state.build_committee_cache(relative_epoch, &self.spec)?; + + let committee_cache = state.committee_cache(relative_epoch)?; + + self.shuffling_cache + .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .ok_or_else(|| Error::AttestationCacheLockTimeout)? + .insert(shuffling_id, committee_cache); + + metrics::stop_timer(committee_building_timer); + + map_fn(&committee_cache) + } + } + /// Returns `true` if the given block root has not been processed. pub fn is_new_block_root(&self, beacon_block_root: &Hash256) -> Result<bool, Error> { Ok(!self diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index ac9d1c4b1..ff47c7a2b 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -374,8 +374,13 @@ where let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis); - let fork_choice = ForkChoice::from_genesis(fc_store, &genesis.beacon_block.message) - .map_err(|e| format!("Unable to build initialize ForkChoice: {:?}", e))?; + let fork_choice = ForkChoice::from_genesis( + fc_store, + genesis.beacon_block_root, + &genesis.beacon_block.message, + &genesis.beacon_state, + ) + .map_err(|e| format!("Unable to build initialize ForkChoice: {:?}", e))?; self.fork_choice = Some(fork_choice); self.genesis_time = Some(genesis.beacon_state.genesis_time); @@ -561,6 +566,7 @@ where observed_attester_slashings: <_>::default(), eth1_chain: self.eth1_chain, genesis_validators_root: canonical_head.beacon_state.genesis_validators_root, + genesis_state_root: canonical_head.beacon_state_root, canonical_head: TimeoutRwLock::new(canonical_head.clone()), genesis_block_root, fork_choice: RwLock::new(fork_choice), diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 96f1c9a84..6eb7bceeb 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -83,6 +83,10 @@ pub enum BeaconChainError { ObservedBlockProducersError(ObservedBlockProducersError), PruningError(PruningError), ArithError(ArithError), + InvalidShufflingId { + shuffling_epoch: Epoch, + head_block_epoch: Epoch, + }, } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/naive_aggregation_pool.rs b/beacon_node/beacon_chain/src/naive_aggregation_pool.rs index c561141a1..247f613a9 100644 --- a/beacon_node/beacon_chain/src/naive_aggregation_pool.rs +++ b/beacon_node/beacon_chain/src/naive_aggregation_pool.rs @@ -1,7 +1,9 @@ use crate::metrics; use std::collections::HashMap; -use types::{Attestation, AttestationData, EthSpec, Slot}; +use tree_hash::TreeHash; +use types::{Attestation, AttestationData, EthSpec, Hash256, Slot}; +type AttestationDataRoot = Hash256; /// The number of slots that will be stored in the pool. /// /// For example, if `SLOTS_RETAINED == 3` and the pool is pruned at slot `6`, then all attestations @@ -53,7 +55,7 @@ pub enum Error { /// A collection of `Attestation` objects, keyed by their `attestation.data`. Enforces that all /// `attestation` are from the same slot. struct AggregatedAttestationMap<E: EthSpec> { - map: HashMap<AttestationData, Attestation<E>>, + map: HashMap<AttestationDataRoot, Attestation<E>>, } impl<E: EthSpec> AggregatedAttestationMap<E> { @@ -87,7 +89,9 @@ impl<E: EthSpec> AggregatedAttestationMap<E> { return Err(Error::MoreThanOneAggregationBitSet(set_bits.len())); } - if let Some(existing_attestation) = self.map.get_mut(&a.data) { + let attestation_data_root = a.data.tree_hash_root(); + + if let Some(existing_attestation) = self.map.get_mut(&attestation_data_root) { if existing_attestation .aggregation_bits .get(committee_index) @@ -107,7 +111,7 @@ impl<E: EthSpec> AggregatedAttestationMap<E> { )); } - self.map.insert(a.data.clone(), a.clone()); + self.map.insert(attestation_data_root, a.clone()); Ok(InsertOutcome::NewAttestationData { committee_index }) } } @@ -115,8 +119,13 @@ impl<E: EthSpec> AggregatedAttestationMap<E> { /// Returns an aggregated `Attestation` with the given `data`, if any. /// /// The given `a.data.slot` must match the slot that `self` was initialized with. - pub fn get(&self, data: &AttestationData) -> Result<Option<Attestation<E>>, Error> { - Ok(self.map.get(data).cloned()) + pub fn get(&self, data: &AttestationData) -> Option<Attestation<E>> { + self.map.get(&data.tree_hash_root()).cloned() + } + + /// Returns an aggregated `Attestation` with the given `root`, if any. + pub fn get_by_root(&self, root: &AttestationDataRoot) -> Option<&Attestation<E>> { + self.map.get(root) } /// Iterate all attestations in `self`. @@ -220,12 +229,19 @@ impl<E: EthSpec> NaiveAggregationPool<E> { } /// Returns an aggregated `Attestation` with the given `data`, if any. - pub fn get(&self, data: &AttestationData) -> Result<Option<Attestation<E>>, Error> { + pub fn get(&self, data: &AttestationData) -> Option<Attestation<E>> { + self.maps.get(&data.slot).and_then(|map| map.get(data)) + } + + /// Returns an aggregated `Attestation` with the given `data`, if any. + pub fn get_by_slot_and_root( + &self, + slot: Slot, + root: &AttestationDataRoot, + ) -> Option<Attestation<E>> { self.maps - .iter() - .find(|(slot, _map)| **slot == data.slot) - .map(|(_slot, map)| map.get(data)) - .unwrap_or_else(|| Ok(None)) + .get(&slot) + .and_then(|map| map.get_by_root(root).cloned()) } /// Iterate all attestations in all slots of `self`. @@ -338,8 +354,7 @@ mod tests { let retrieved = pool .get(&a.data) - .expect("should not error while getting attestation") - .expect("should get an attestation"); + .expect("should not error while getting attestation"); assert_eq!( retrieved, a, "retrieved attestation should equal the one inserted" @@ -378,8 +393,7 @@ mod tests { let retrieved = pool .get(&a_0.data) - .expect("should not error while getting attestation") - .expect("should get an attestation"); + .expect("should not error while getting attestation"); let mut a_01 = a_0.clone(); a_01.aggregate(&a_1); @@ -408,8 +422,7 @@ mod tests { assert_eq!( pool.get(&a_0.data) - .expect("should not error while getting attestation") - .expect("should get an attestation"), + .expect("should not error while getting attestation"), retrieved, "should not have aggregated different attestation data" ); diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index d8b6e8706..b76adf05e 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -1,6 +1,6 @@ use crate::metrics; use lru::LruCache; -use types::{beacon_state::CommitteeCache, Epoch, Hash256}; +use types::{beacon_state::CommitteeCache, Epoch, Hash256, ShufflingId}; /// The size of the LRU cache that stores committee caches for quicker verification. /// @@ -14,7 +14,7 @@ const CACHE_SIZE: usize = 16; /// It has been named `ShufflingCache` because `CommitteeCacheCache` is a bit weird and looks like /// a find/replace error. pub struct ShufflingCache { - cache: LruCache<(Epoch, Hash256), CommitteeCache>, + cache: LruCache<ShufflingId, CommitteeCache>, } impl ShufflingCache { @@ -24,8 +24,8 @@ impl ShufflingCache { } } - pub fn get(&mut self, epoch: Epoch, root: Hash256) -> Option<&CommitteeCache> { - let opt = self.cache.get(&(epoch, root)); + pub fn get(&mut self, key: &ShufflingId) -> Option<&CommitteeCache> { + let opt = self.cache.get(key); if opt.is_some() { metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); @@ -36,11 +36,37 @@ impl ShufflingCache { opt } - pub fn insert(&mut self, epoch: Epoch, root: Hash256, committee_cache: &CommitteeCache) { - let key = (epoch, root); + pub fn contains(&self, key: &ShufflingId) -> bool { + self.cache.contains(key) + } + pub fn insert(&mut self, key: ShufflingId, committee_cache: &CommitteeCache) { if !self.cache.contains(&key) { self.cache.put(key, committee_cache.clone()); } } } + +/// Contains the shuffling IDs for a beacon block. +pub struct BlockShufflingIds { + pub current: ShufflingId, + pub next: ShufflingId, + pub block_root: Hash256, +} + +impl BlockShufflingIds { + /// Returns the shuffling ID for the given epoch. + /// + /// Returns `None` if `epoch` is prior to `self.current.shuffling_epoch`. + pub fn id_for_epoch(&self, epoch: Epoch) -> Option<ShufflingId> { + if epoch == self.current.shuffling_epoch { + Some(self.current.clone()) + } else if epoch == self.next.shuffling_epoch { + Some(self.next.clone()) + } else if epoch > self.next.shuffling_epoch { + Some(ShufflingId::from_components(epoch, self.block_root)) + } else { + None + } + } +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 8690c2e8d..2bad5f892 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -26,9 +26,11 @@ use store::{config::StoreConfig, BlockReplay, HotColdDB, ItemStore, LevelDB, Mem use tempfile::{tempdir, TempDir}; use tree_hash::TreeHash; use types::{ - AggregateSignature, Attestation, BeaconState, BeaconStateHash, ChainSpec, Domain, Epoch, - EthSpec, Hash256, Keypair, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, - SignedBeaconBlockHash, SignedRoot, Slot, SubnetId, + AggregateSignature, Attestation, AttestationData, AttesterSlashing, BeaconState, + BeaconStateHash, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, IndexedAttestation, + Keypair, ProposerSlashing, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedBeaconBlockHash, SignedRoot, SignedVoluntaryExit, Slot, SubnetId, VariableList, + VoluntaryExit, }; pub use types::test_utils::generate_deterministic_keypairs; @@ -129,7 +131,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorEphemeralHarnessType<E>> { let decorator = slog_term::PlainDecorator::new(slog_term::TestStdoutWriter); let drain = slog_term::FullFormat::new(decorator).build(); - let debug_level = slog::LevelFilter::new(drain, slog::Level::Debug); + let debug_level = slog::LevelFilter::new(drain, slog::Level::Critical); let log = slog::Logger::root(std::sync::Mutex::new(debug_level).fuse(), o!()); let config = StoreConfig::default(); @@ -193,7 +195,7 @@ impl<E: EthSpec> BeaconChainHarness<NullMigratorEphemeralHarnessType<E>> { let decorator = slog_term::PlainDecorator::new(slog_term::TestStdoutWriter); let drain = slog_term::FullFormat::new(decorator).build(); - let debug_level = slog::LevelFilter::new(drain, slog::Level::Debug); + let debug_level = slog::LevelFilter::new(drain, slog::Level::Critical); let log = slog::Logger::root(std::sync::Mutex::new(debug_level).fuse(), o!()); let store = HotColdDB::open_ephemeral(config, spec.clone(), log.clone()).unwrap(); @@ -238,7 +240,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { let decorator = slog_term::PlainDecorator::new(slog_term::TestStdoutWriter); let drain = slog_term::FullFormat::new(decorator).build(); - let debug_level = slog::LevelFilter::new(drain, slog::Level::Debug); + let debug_level = slog::LevelFilter::new(drain, slog::Level::Critical); let log = slog::Logger::root(std::sync::Mutex::new(debug_level).fuse(), o!()); let chain = BeaconChainBuilder::new(eth_spec_instance) @@ -397,7 +399,7 @@ where // If we produce two blocks for the same slot, they hash up to the same value and // BeaconChain errors out with `BlockIsAlreadyKnown`. Vary the graffiti so that we produce // different blocks each time. - self.chain.set_graffiti(self.rng.gen::<[u8; 32]>()); + self.chain.set_graffiti(self.rng.gen::<[u8; 32]>().into()); let randao_reveal = { let epoch = slot.epoch(E::slots_per_epoch()); @@ -442,8 +444,8 @@ where let committee_count = state.get_committee_count_at_slot(state.slot).unwrap(); state - .get_beacon_committees_at_slot(state.slot) - .unwrap() + .get_beacon_committees_at_slot(attestation_slot) + .expect("should get committees") .iter() .map(|bc| { bc.committee @@ -570,7 +572,6 @@ where let aggregate = self .chain .get_aggregated_attestation(&attestation.data) - .unwrap() .unwrap_or_else(|| { committee_attestations.iter().skip(1).fold(attestation.clone(), |mut agg, (att, _)| { agg.aggregate(att); @@ -601,6 +602,94 @@ where .collect() } + pub fn make_attester_slashing(&self, validator_indices: Vec<u64>) -> AttesterSlashing<E> { + let mut attestation_1 = IndexedAttestation { + attesting_indices: VariableList::new(validator_indices).unwrap(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + target: Checkpoint { + root: Hash256::zero(), + epoch: Epoch::new(0), + }, + source: Checkpoint { + root: Hash256::zero(), + epoch: Epoch::new(0), + }, + }, + signature: AggregateSignature::infinity(), + }; + + let mut attestation_2 = attestation_1.clone(); + attestation_2.data.index += 1; + + for attestation in &mut [&mut attestation_1, &mut attestation_2] { + for &i in &attestation.attesting_indices { + let sk = &self.validators_keypairs[i as usize].sk; + + let fork = self.chain.head_info().unwrap().fork; + let genesis_validators_root = self.chain.genesis_validators_root; + + let domain = self.chain.spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + &fork, + genesis_validators_root, + ); + let message = attestation.data.signing_root(domain); + + attestation.signature.add_assign(&sk.sign(message)); + } + } + + AttesterSlashing { + attestation_1, + attestation_2, + } + } + + pub fn make_proposer_slashing(&self, validator_index: u64) -> ProposerSlashing { + let mut block_header_1 = self + .chain + .head_beacon_block() + .unwrap() + .message + .block_header(); + block_header_1.proposer_index = validator_index; + + let mut block_header_2 = block_header_1.clone(); + block_header_2.state_root = Hash256::zero(); + + let sk = &self.validators_keypairs[validator_index as usize].sk; + let fork = self.chain.head_info().unwrap().fork; + let genesis_validators_root = self.chain.genesis_validators_root; + + let mut signed_block_headers = vec![block_header_1, block_header_2] + .into_iter() + .map(|block_header| { + block_header.sign::<E>(&sk, &fork, genesis_validators_root, &self.chain.spec) + }) + .collect::<Vec<_>>(); + + ProposerSlashing { + signed_header_2: signed_block_headers.remove(1), + signed_header_1: signed_block_headers.remove(0), + } + } + + pub fn make_voluntary_exit(&self, validator_index: u64, epoch: Epoch) -> SignedVoluntaryExit { + let sk = &self.validators_keypairs[validator_index as usize].sk; + let fork = self.chain.head_info().unwrap().fork; + let genesis_validators_root = self.chain.genesis_validators_root; + + VoluntaryExit { + epoch, + validator_index, + } + .sign(sk, &fork, genesis_validators_root, &self.chain.spec) + } + pub fn process_block(&self, slot: Slot, block: SignedBeaconBlock<E>) -> SignedBeaconBlockHash { assert_eq!(self.chain.slot().unwrap(), slot); let block_hash: SignedBeaconBlockHash = self.chain.process_block(block).unwrap().into(); @@ -612,7 +701,10 @@ where for (unaggregated_attestations, maybe_signed_aggregate) in attestations.into_iter() { for (attestation, subnet_id) in unaggregated_attestations { self.chain - .verify_unaggregated_attestation_for_gossip(attestation.clone(), subnet_id) + .verify_unaggregated_attestation_for_gossip( + attestation.clone(), + Some(subnet_id), + ) .unwrap() .add_to_pool(&self.chain) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 937850751..35c87c0d9 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -570,7 +570,7 @@ fn unaggregated_gossip_verification() { matches!( harness .chain - .verify_unaggregated_attestation_for_gossip($attn_getter, $subnet_getter) + .verify_unaggregated_attestation_for_gossip($attn_getter, Some($subnet_getter)) .err() .expect(&format!( "{} should error during verify_unaggregated_attestation_for_gossip", @@ -837,7 +837,7 @@ fn unaggregated_gossip_verification() { harness .chain - .verify_unaggregated_attestation_for_gossip(valid_attestation.clone(), subnet_id) + .verify_unaggregated_attestation_for_gossip(valid_attestation.clone(), Some(subnet_id)) .expect("valid attestation should be verified"); /* @@ -926,6 +926,6 @@ fn attestation_that_skips_epochs() { harness .chain - .verify_unaggregated_attestation_for_gossip(attestation, subnet_id) + .verify_unaggregated_attestation_for_gossip(attestation, Some(subnet_id)) .expect("should gossip verify attestation that skips slots"); } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index caa2f9d6c..e9006a626 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -326,7 +326,7 @@ fn epoch_boundary_state_attestation_processing() { let res = harness .chain - .verify_unaggregated_attestation_for_gossip(attestation.clone(), subnet_id); + .verify_unaggregated_attestation_for_gossip(attestation.clone(), Some(subnet_id)); let current_slot = harness.chain.slot().expect("should get slot"); let expected_attestation_slot = attestation.data.slot; diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 12f1c4364..721eb4091 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -463,7 +463,7 @@ fn attestations_with_increasing_slots() { for (attestation, subnet_id) in attestations.into_iter().flatten() { let res = harness .chain - .verify_unaggregated_attestation_for_gossip(attestation.clone(), subnet_id); + .verify_unaggregated_attestation_for_gossip(attestation.clone(), Some(subnet_id)); let current_slot = harness.chain.slot().expect("should get slot"); let expected_attestation_slot = attestation.data.slot; diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index ba98eb946..797d7adb4 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -14,7 +14,6 @@ store = { path = "../store" } network = { path = "../network" } timer = { path = "../timer" } eth2_libp2p = { path = "../eth2_libp2p" } -rest_api = { path = "../rest_api" } parking_lot = "0.11.0" websocket_server = { path = "../websocket_server" } prometheus = "0.9.0" @@ -42,3 +41,5 @@ lighthouse_metrics = { path = "../../common/lighthouse_metrics" } time = "0.2.16" bus = "2.2.3" directory = {path = "../../common/directory"} +http_api = { path = "../http_api" } +http_metrics = { path = "../http_metrics" } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 15cd97ea8..05cc6aa6d 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -13,15 +13,14 @@ use beacon_chain::{ use bus::Bus; use environment::RuntimeContext; use eth1::{Config as Eth1Config, Service as Eth1Service}; -use eth2_config::Eth2Config; use eth2_libp2p::NetworkGlobals; use genesis::{interop_genesis_state, Eth1GenesisService}; use network::{NetworkConfig, NetworkMessage, NetworkService}; use parking_lot::Mutex; -use slog::info; +use slog::{debug, info}; use ssz::Decode; use std::net::SocketAddr; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use timer::spawn_timer; @@ -61,7 +60,10 @@ pub struct ClientBuilder<T: BeaconChainTypes> { event_handler: Option<T::EventHandler>, network_globals: Option<Arc<NetworkGlobals<T::EthSpec>>>, network_send: Option<UnboundedSender<NetworkMessage<T::EthSpec>>>, - http_listen_addr: Option<SocketAddr>, + db_path: Option<PathBuf>, + freezer_db_path: Option<PathBuf>, + http_api_config: http_api::Config, + http_metrics_config: http_metrics::Config, websocket_listen_addr: Option<SocketAddr>, eth_spec_instance: T::EthSpec, } @@ -103,7 +105,10 @@ where event_handler: None, network_globals: None, network_send: None, - http_listen_addr: None, + db_path: None, + freezer_db_path: None, + http_api_config: <_>::default(), + http_metrics_config: <_>::default(), websocket_listen_addr: None, eth_spec_instance, } @@ -280,55 +285,16 @@ where Ok(self) } - /// Immediately starts the beacon node REST API http server. - pub fn http_server( - mut self, - client_config: &ClientConfig, - eth2_config: &Eth2Config, - events: Arc<Mutex<Bus<SignedBeaconBlockHash>>>, - ) -> Result<Self, String> { - let beacon_chain = self - .beacon_chain - .clone() - .ok_or_else(|| "http_server requires a beacon chain")?; - let context = self - .runtime_context - .as_ref() - .ok_or_else(|| "http_server requires a runtime_context")? - .service_context("http".into()); - let network_globals = self - .network_globals - .clone() - .ok_or_else(|| "http_server requires a libp2p network")?; - let network_send = self - .network_send - .clone() - .ok_or_else(|| "http_server requires a libp2p network sender")?; + /// Provides configuration for the HTTP API. + pub fn http_api_config(mut self, config: http_api::Config) -> Self { + self.http_api_config = config; + self + } - let network_info = rest_api::NetworkInfo { - network_globals, - network_chan: network_send, - }; - - let listening_addr = rest_api::start_server( - context.executor, - &client_config.rest_api, - beacon_chain, - network_info, - client_config - .create_db_path() - .map_err(|_| "unable to read data dir")?, - client_config - .create_freezer_db_path() - .map_err(|_| "unable to read freezer DB dir")?, - eth2_config.clone(), - events, - ) - .map_err(|e| format!("Failed to start HTTP API: {:?}", e))?; - - self.http_listen_addr = Some(listening_addr); - - Ok(self) + /// Provides configuration for the HTTP server that serves Prometheus metrics. + pub fn http_metrics_config(mut self, config: http_metrics::Config) -> Self { + self.http_metrics_config = config; + self } /// Immediately starts the service that periodically logs information each slot. @@ -367,25 +333,85 @@ where /// specified. /// /// If type inference errors are being raised, see the comment on the definition of `Self`. + #[allow(clippy::type_complexity)] pub fn build( self, - ) -> Client< - Witness< - TStoreMigrator, - TSlotClock, - TEth1Backend, - TEthSpec, - TEventHandler, - THotStore, - TColdStore, + ) -> Result< + Client< + Witness< + TStoreMigrator, + TSlotClock, + TEth1Backend, + TEthSpec, + TEventHandler, + THotStore, + TColdStore, + >, >, + String, > { - Client { + let runtime_context = self + .runtime_context + .as_ref() + .ok_or_else(|| "build requires a runtime context".to_string())?; + let log = runtime_context.log().clone(); + + let http_api_listen_addr = if self.http_api_config.enabled { + let ctx = Arc::new(http_api::Context { + config: self.http_api_config.clone(), + chain: self.beacon_chain.clone(), + network_tx: self.network_send.clone(), + network_globals: self.network_globals.clone(), + log: log.clone(), + }); + + let exit = runtime_context.executor.exit(); + + let (listen_addr, server) = http_api::serve(ctx, exit) + .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; + + runtime_context + .clone() + .executor + .spawn_without_exit(async move { server.await }, "http-api"); + + Some(listen_addr) + } else { + info!(log, "HTTP server is disabled"); + None + }; + + let http_metrics_listen_addr = if self.http_metrics_config.enabled { + let ctx = Arc::new(http_metrics::Context { + config: self.http_metrics_config.clone(), + chain: self.beacon_chain.clone(), + db_path: self.db_path.clone(), + freezer_db_path: self.freezer_db_path.clone(), + log: log.clone(), + }); + + let exit = runtime_context.executor.exit(); + + let (listen_addr, server) = http_metrics::serve(ctx, exit) + .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; + + runtime_context + .executor + .spawn_without_exit(async move { server.await }, "http-api"); + + Some(listen_addr) + } else { + debug!(log, "Metrics server is disabled"); + None + }; + + Ok(Client { beacon_chain: self.beacon_chain, network_globals: self.network_globals, - http_listen_addr: self.http_listen_addr, + http_api_listen_addr, + http_metrics_listen_addr, websocket_listen_addr: self.websocket_listen_addr, - } + }) } } @@ -520,6 +546,9 @@ where .clone() .ok_or_else(|| "disk_store requires a chain spec".to_string())?; + self.db_path = Some(hot_path.into()); + self.freezer_db_path = Some(cold_path.into()); + let store = HotColdDB::open(hot_path, cold_path, config, spec, context.log().clone()) .map_err(|e| format!("Unable to open database: {:?}", e))?; self.store = Some(Arc::new(store)); diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index fdcd3d6e8..0cf90d6b4 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -62,10 +62,11 @@ pub struct Config { pub genesis: ClientGenesis, pub store: store::StoreConfig, pub network: network::NetworkConfig, - pub rest_api: rest_api::Config, pub chain: beacon_chain::ChainConfig, pub websocket_server: websocket_server::Config, pub eth1: eth1::Config, + pub http_api: http_api::Config, + pub http_metrics: http_metrics::Config, } impl Default for Config { @@ -79,7 +80,6 @@ impl Default for Config { store: <_>::default(), network: NetworkConfig::default(), chain: <_>::default(), - rest_api: <_>::default(), websocket_server: <_>::default(), spec_constants: TESTNET_SPEC_CONSTANTS.into(), dummy_eth1_backend: false, @@ -87,6 +87,8 @@ impl Default for Config { eth1: <_>::default(), disabled_forks: Vec::new(), graffiti: Graffiti::default(), + http_api: <_>::default(), + http_metrics: <_>::default(), } } } diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index da670ff13..6b721aee9 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -23,7 +23,10 @@ pub use eth2_config::Eth2Config; pub struct Client<T: BeaconChainTypes> { beacon_chain: Option<Arc<BeaconChain<T>>>, network_globals: Option<Arc<NetworkGlobals<T::EthSpec>>>, - http_listen_addr: Option<SocketAddr>, + /// Listen address for the standard eth2.0 API, if the service was started. + http_api_listen_addr: Option<SocketAddr>, + /// Listen address for the HTTP server which serves Prometheus metrics. + http_metrics_listen_addr: Option<SocketAddr>, websocket_listen_addr: Option<SocketAddr>, } @@ -33,9 +36,14 @@ impl<T: BeaconChainTypes> Client<T> { self.beacon_chain.clone() } - /// Returns the address of the client's HTTP API server, if it was started. - pub fn http_listen_addr(&self) -> Option<SocketAddr> { - self.http_listen_addr + /// Returns the address of the client's standard eth2.0 API server, if it was started. + pub fn http_api_listen_addr(&self) -> Option<SocketAddr> { + self.http_api_listen_addr + } + + /// Returns the address of the client's HTTP Prometheus metrics server, if it was started. + pub fn http_metrics_listen_addr(&self) -> Option<SocketAddr> { + self.http_metrics_listen_addr } /// Returns the address of the client's WebSocket API server, if it was started. diff --git a/beacon_node/eth1/src/http.rs b/beacon_node/eth1/src/http.rs index 6dffdaa7c..e8f7d23a0 100644 --- a/beacon_node/eth1/src/http.rs +++ b/beacon_node/eth1/src/http.rs @@ -39,19 +39,34 @@ pub enum Eth1NetworkId { Custom(u64), } +impl Into<u64> for Eth1NetworkId { + fn into(self) -> u64 { + match self { + Eth1NetworkId::Mainnet => 1, + Eth1NetworkId::Goerli => 5, + Eth1NetworkId::Custom(id) => id, + } + } +} + +impl From<u64> for Eth1NetworkId { + fn from(id: u64) -> Self { + let into = |x: Eth1NetworkId| -> u64 { x.into() }; + match id { + id if id == into(Eth1NetworkId::Mainnet) => Eth1NetworkId::Mainnet, + id if id == into(Eth1NetworkId::Goerli) => Eth1NetworkId::Goerli, + id => Eth1NetworkId::Custom(id), + } + } +} + impl FromStr for Eth1NetworkId { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "1" => Ok(Eth1NetworkId::Mainnet), - "5" => Ok(Eth1NetworkId::Goerli), - custom => { - let network_id = u64::from_str_radix(custom, 10) - .map_err(|e| format!("Failed to parse eth1 network id {}", e))?; - Ok(Eth1NetworkId::Custom(network_id)) - } - } + u64::from_str_radix(s, 10) + .map(Into::into) + .map_err(|e| format!("Failed to parse eth1 network id {}", e)) } } diff --git a/beacon_node/eth1/src/lib.rs b/beacon_node/eth1/src/lib.rs index f5f018bd1..a7aba85a2 100644 --- a/beacon_node/eth1/src/lib.rs +++ b/beacon_node/eth1/src/lib.rs @@ -13,4 +13,6 @@ pub use block_cache::{BlockCache, Eth1Block}; pub use deposit_cache::DepositCache; pub use deposit_log::DepositLog; pub use inner::SszEth1Cache; -pub use service::{BlockCacheUpdateOutcome, Config, DepositCacheUpdateOutcome, Error, Service}; +pub use service::{ + BlockCacheUpdateOutcome, Config, DepositCacheUpdateOutcome, Error, Service, DEFAULT_NETWORK_ID, +}; diff --git a/beacon_node/rest_api/Cargo.toml b/beacon_node/http_api/Cargo.toml similarity index 51% rename from beacon_node/rest_api/Cargo.toml rename to beacon_node/http_api/Cargo.toml index 38a5a1e7d..828d26deb 100644 --- a/beacon_node/rest_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -1,50 +1,34 @@ [package] -name = "rest_api" -version = "0.2.0" -authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>", "Luke Anderson <luke@sigmaprime.io>"] +name = "http_api" +version = "0.1.0" +authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [dependencies] -bls = { path = "../../crypto/bls" } -rest_types = { path = "../../common/rest_types" } +warp = "0.2.5" +serde = { version = "1.0.110", features = ["derive"] } +tokio = { version = "0.2.21", features = ["sync"] } +parking_lot = "0.11.0" +types = { path = "../../consensus/types" } +hex = "0.4.2" beacon_chain = { path = "../beacon_chain" } +eth2 = { path = "../../common/eth2", features = ["lighthouse"] } +slog = "2.5.2" network = { path = "../network" } eth2_libp2p = { path = "../eth2_libp2p" } -store = { path = "../store" } -serde = { version = "1.0.110", features = ["derive"] } -serde_json = "1.0.52" -serde_yaml = "0.8.11" -slog = "2.5.2" -slog-term = "2.5.0" -slog-async = "2.5.0" -eth2_ssz = "0.1.2" -eth2_ssz_derive = "0.1.0" +eth1 = { path = "../eth1" } +fork_choice = { path = "../../consensus/fork_choice" } state_processing = { path = "../../consensus/state_processing" } -types = { path = "../../consensus/types" } -http = "0.2.1" -hyper = "0.13.5" -tokio = { version = "0.2.21", features = ["sync"] } -url = "2.1.1" -lazy_static = "1.4.0" -eth2_config = { path = "../../common/eth2_config" } -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -slot_clock = { path = "../../common/slot_clock" } -hex = "0.4.2" -parking_lot = "0.11.0" -futures = "0.3.5" -operation_pool = { path = "../operation_pool" } -environment = { path = "../../lighthouse/environment" } -uhttp_sse = "0.5.1" -bus = "2.2.3" -itertools = "0.9.0" lighthouse_version = { path = "../../common/lighthouse_version" } +lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +lazy_static = "1.4.0" +warp_utils = { path = "../../common/warp_utils" } +slot_clock = { path = "../../common/slot_clock" } [dev-dependencies] -assert_matches = "1.3.0" -remote_beacon_node = { path = "../../common/remote_beacon_node" } -node_test_rig = { path = "../../testing/node_test_rig" } -tree_hash = "0.1.0" - -[features] -fake_crypto = [] +store = { path = "../store" } +environment = { path = "../../lighthouse/environment" } +tree_hash = { path = "../../consensus/tree_hash" } +discv5 = { version = "0.1.0-alpha.10", features = ["libp2p"] } diff --git a/beacon_node/http_api/src/beacon_proposer_cache.rs b/beacon_node/http_api/src/beacon_proposer_cache.rs new file mode 100644 index 000000000..b062119e5 --- /dev/null +++ b/beacon_node/http_api/src/beacon_proposer_cache.rs @@ -0,0 +1,185 @@ +use crate::metrics; +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use eth2::types::ProposerData; +use fork_choice::ProtoBlock; +use slot_clock::SlotClock; +use state_processing::per_slot_processing; +use types::{BeaconState, Epoch, EthSpec, Hash256, PublicKeyBytes}; + +/// This sets a maximum bound on the number of epochs to skip whilst instantiating the cache for +/// the first time. +const EPOCHS_TO_SKIP: u64 = 2; + +/// Caches the beacon block proposers for a given `epoch` and `epoch_boundary_root`. +/// +/// This cache is only able to contain a single set of proposers and is only +/// intended to cache the proposers for the current epoch according to the head +/// of the chain. A change in epoch or re-org to a different chain may cause a +/// cache miss and rebuild. +pub struct BeaconProposerCache { + epoch: Epoch, + decision_block_root: Hash256, + proposers: Vec<ProposerData>, +} + +impl BeaconProposerCache { + /// Create a new cache for the current epoch of the `chain`. + pub fn new<T: BeaconChainTypes>(chain: &BeaconChain<T>) -> Result<Self, BeaconChainError> { + let head_root = chain.head_beacon_block_root()?; + let head_block = chain + .fork_choice + .read() + .get_block(&head_root) + .ok_or_else(|| BeaconChainError::MissingBeaconBlock(head_root))?; + + // If the head epoch is more than `EPOCHS_TO_SKIP` in the future, just build the cache at + // the epoch of the head. This prevents doing a massive amount of skip slots when starting + // a new database from genesis. + let epoch = { + let epoch_now = chain + .epoch() + .unwrap_or_else(|_| chain.spec.genesis_slot.epoch(T::EthSpec::slots_per_epoch())); + let head_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); + if epoch_now > head_epoch + EPOCHS_TO_SKIP { + head_epoch + } else { + epoch_now + } + }; + + Self::for_head_block(chain, epoch, head_root, head_block) + } + + /// Create a new cache that contains the shuffling for `current_epoch`, + /// assuming that `head_root` and `head_block` represents the most recent + /// canonical block. + fn for_head_block<T: BeaconChainTypes>( + chain: &BeaconChain<T>, + current_epoch: Epoch, + head_root: Hash256, + head_block: ProtoBlock, + ) -> Result<Self, BeaconChainError> { + let _timer = metrics::start_timer(&metrics::HTTP_API_BEACON_PROPOSER_CACHE_TIMES); + + let mut head_state = chain + .get_state(&head_block.state_root, Some(head_block.slot))? + .ok_or_else(|| BeaconChainError::MissingBeaconState(head_block.state_root))?; + + let decision_block_root = Self::decision_block_root(current_epoch, head_root, &head_state)?; + + // We *must* skip forward to the current epoch to obtain valid proposer + // duties. We cannot skip to the previous epoch, like we do with + // attester duties. + while head_state.current_epoch() < current_epoch { + // Skip slots until the current epoch, providing `Hash256::zero()` as the state root + // since we don't require it to be valid to identify producers. + per_slot_processing(&mut head_state, Some(Hash256::zero()), &chain.spec)?; + } + + let proposers = current_epoch + .slot_iter(T::EthSpec::slots_per_epoch()) + .map(|slot| { + head_state + .get_beacon_proposer_index(slot, &chain.spec) + .map_err(BeaconChainError::from) + .and_then(|i| { + let pubkey = chain + .validator_pubkey(i)? + .ok_or_else(|| BeaconChainError::ValidatorPubkeyCacheIncomplete(i))?; + + Ok(ProposerData { + pubkey: PublicKeyBytes::from(pubkey), + slot, + }) + }) + }) + .collect::<Result<_, _>>()?; + + Ok(Self { + epoch: current_epoch, + decision_block_root, + proposers, + }) + } + + /// Returns a block root which can be used to key the shuffling obtained from the following + /// parameters: + /// + /// - `shuffling_epoch`: the epoch for which the shuffling pertains. + /// - `head_block_root`: the block root at the head of the chain. + /// - `head_block_state`: the state of `head_block_root`. + pub fn decision_block_root<E: EthSpec>( + shuffling_epoch: Epoch, + head_block_root: Hash256, + head_block_state: &BeaconState<E>, + ) -> Result<Hash256, BeaconChainError> { + let decision_slot = shuffling_epoch + .start_slot(E::slots_per_epoch()) + .saturating_sub(1_u64); + + // If decision slot is equal to or ahead of the head, the block root is the head block root + if decision_slot >= head_block_state.slot { + Ok(head_block_root) + } else { + head_block_state + .get_block_root(decision_slot) + .map(|root| *root) + .map_err(Into::into) + } + } + + /// Return the proposers for the given `Epoch`. + /// + /// The cache may be rebuilt if: + /// + /// - The epoch has changed since the last cache build. + /// - There has been a re-org that crosses an epoch boundary. + pub fn get_proposers<T: BeaconChainTypes>( + &mut self, + chain: &BeaconChain<T>, + epoch: Epoch, + ) -> Result<Vec<ProposerData>, warp::Rejection> { + let current_epoch = chain + .slot_clock + .now_or_genesis() + .ok_or_else(|| { + warp_utils::reject::custom_server_error("unable to read slot clock".to_string()) + })? + .epoch(T::EthSpec::slots_per_epoch()); + + // Disallow requests that are outside the current epoch. This ensures the cache doesn't get + // washed-out with old values. + if current_epoch != epoch { + return Err(warp_utils::reject::custom_bad_request(format!( + "requested epoch is {} but only current epoch {} is allowed", + epoch, current_epoch + ))); + } + + let (head_block_root, head_decision_block_root) = chain + .with_head(|head| { + Self::decision_block_root(current_epoch, head.beacon_block_root, &head.beacon_state) + .map(|decision_root| (head.beacon_block_root, decision_root)) + }) + .map_err(warp_utils::reject::beacon_chain_error)?; + + let head_block = chain + .fork_choice + .read() + .get_block(&head_block_root) + .ok_or_else(|| BeaconChainError::MissingBeaconBlock(head_block_root)) + .map_err(warp_utils::reject::beacon_chain_error)?; + + // Rebuild the cache if this call causes a cache-miss. + if self.epoch != current_epoch || self.decision_block_root != head_decision_block_root { + metrics::inc_counter(&metrics::HTTP_API_BEACON_PROPOSER_CACHE_MISSES_TOTAL); + + *self = Self::for_head_block(chain, current_epoch, head_block_root, head_block) + .map_err(warp_utils::reject::beacon_chain_error)?; + } else { + metrics::inc_counter(&metrics::HTTP_API_BEACON_PROPOSER_CACHE_HITS_TOTAL); + } + + Ok(self.proposers.clone()) + } +} diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs new file mode 100644 index 000000000..5e358a2d6 --- /dev/null +++ b/beacon_node/http_api/src/block_id.rs @@ -0,0 +1,87 @@ +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::BlockId as CoreBlockId; +use std::str::FromStr; +use types::{Hash256, SignedBeaconBlock, Slot}; + +/// Wraps `eth2::types::BlockId` and provides a simple way to obtain a block or root for a given +/// `BlockId`. +#[derive(Debug)] +pub struct BlockId(pub CoreBlockId); + +impl BlockId { + pub fn from_slot(slot: Slot) -> Self { + Self(CoreBlockId::Slot(slot)) + } + + pub fn from_root(root: Hash256) -> Self { + Self(CoreBlockId::Root(root)) + } + + /// Return the block root identified by `self`. + pub fn root<T: BeaconChainTypes>( + &self, + chain: &BeaconChain<T>, + ) -> Result<Hash256, warp::Rejection> { + match &self.0 { + CoreBlockId::Head => chain + .head_info() + .map(|head| head.block_root) + .map_err(warp_utils::reject::beacon_chain_error), + CoreBlockId::Genesis => Ok(chain.genesis_block_root), + CoreBlockId::Finalized => chain + .head_info() + .map(|head| head.finalized_checkpoint.root) + .map_err(warp_utils::reject::beacon_chain_error), + CoreBlockId::Justified => chain + .head_info() + .map(|head| head.current_justified_checkpoint.root) + .map_err(warp_utils::reject::beacon_chain_error), + CoreBlockId::Slot(slot) => chain + .block_root_at_slot(*slot) + .map_err(warp_utils::reject::beacon_chain_error) + .and_then(|root_opt| { + root_opt.ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon block at slot {}", + slot + )) + }) + }), + CoreBlockId::Root(root) => Ok(*root), + } + } + + /// Return the `SignedBeaconBlock` identified by `self`. + pub fn block<T: BeaconChainTypes>( + &self, + chain: &BeaconChain<T>, + ) -> Result<SignedBeaconBlock<T::EthSpec>, warp::Rejection> { + match &self.0 { + CoreBlockId::Head => chain + .head_beacon_block() + .map_err(warp_utils::reject::beacon_chain_error), + _ => { + let root = self.root(chain)?; + chain + .get_block(&root) + .map_err(warp_utils::reject::beacon_chain_error) + .and_then(|root_opt| { + root_opt.ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon block with root {}", + root + )) + }) + }) + } + } + } +} + +impl FromStr for BlockId { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + CoreBlockId::from_str(s).map(Self) + } +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs new file mode 100644 index 000000000..b23937b5d --- /dev/null +++ b/beacon_node/http_api/src/lib.rs @@ -0,0 +1,1749 @@ +//! This crate contains a HTTP server which serves the endpoints listed here: +//! +//! https://github.com/ethereum/eth2.0-APIs +//! +//! There are also some additional, non-standard endpoints behind the `/lighthouse/` path which are +//! used for development. + +mod beacon_proposer_cache; +mod block_id; +mod metrics; +mod state_id; +mod validator_inclusion; + +use beacon_chain::{ + observed_operations::ObservationOutcome, AttestationError as AttnError, BeaconChain, + BeaconChainError, BeaconChainTypes, +}; +use beacon_proposer_cache::BeaconProposerCache; +use block_id::BlockId; +use eth2::{ + types::{self as api_types, ValidatorId}, + StatusCode, +}; +use eth2_libp2p::{types::SyncState, NetworkGlobals, PubsubMessage}; +use lighthouse_version::version_with_platform; +use network::NetworkMessage; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use slog::{crit, error, info, trace, warn, Logger}; +use slot_clock::SlotClock; +use state_id::StateId; +use state_processing::per_slot_processing; +use std::borrow::Cow; +use std::convert::TryInto; +use std::future::Future; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use types::{ + Attestation, AttestationDuty, AttesterSlashing, CloneConfig, CommitteeCache, Epoch, EthSpec, + Hash256, ProposerSlashing, PublicKey, RelativeEpoch, SignedAggregateAndProof, + SignedBeaconBlock, SignedVoluntaryExit, Slot, YamlConfig, +}; +use warp::Filter; + +const API_PREFIX: &str = "eth"; +const API_VERSION: &str = "v1"; + +/// If the node is within this many epochs from the head, we declare it to be synced regardless of +/// the network sync state. +/// +/// This helps prevent attacks where nodes can convince us that we're syncing some non-existent +/// finalized head. +const SYNC_TOLERANCE_EPOCHS: u64 = 8; + +/// A wrapper around all the items required to spawn the HTTP server. +/// +/// The server will gracefully handle the case where any fields are `None`. +pub struct Context<T: BeaconChainTypes> { + pub config: Config, + pub chain: Option<Arc<BeaconChain<T>>>, + pub network_tx: Option<UnboundedSender<NetworkMessage<T::EthSpec>>>, + pub network_globals: Option<Arc<NetworkGlobals<T::EthSpec>>>, + pub log: Logger, +} + +/// Configuration for the HTTP server. +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub enabled: bool, + pub listen_addr: Ipv4Addr, + pub listen_port: u16, + pub allow_origin: Option<String>, +} + +impl Default for Config { + fn default() -> Self { + Self { + enabled: false, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 5052, + allow_origin: None, + } + } +} + +#[derive(Debug)] +pub enum Error { + Warp(warp::Error), + Other(String), +} + +impl From<warp::Error> for Error { + fn from(e: warp::Error) -> Self { + Error::Warp(e) + } +} + +impl From<String> for Error { + fn from(e: String) -> Self { + Error::Other(e) + } +} + +/// Creates a `warp` logging wrapper which we use to create `slog` logs. +pub fn slog_logging( + log: Logger, +) -> warp::filters::log::Log<impl Fn(warp::filters::log::Info) + Clone> { + warp::log::custom(move |info| { + match info.status() { + status if status == StatusCode::OK || status == StatusCode::NOT_FOUND => { + trace!( + log, + "Processed HTTP API request"; + "elapsed" => format!("{:?}", info.elapsed()), + "status" => status.to_string(), + "path" => info.path(), + "method" => info.method().to_string(), + ); + } + status => { + warn!( + log, + "Error processing HTTP API request"; + "elapsed" => format!("{:?}", info.elapsed()), + "status" => status.to_string(), + "path" => info.path(), + "method" => info.method().to_string(), + ); + } + }; + }) +} + +/// Creates a `warp` logging wrapper which we use for Prometheus metrics (not necessarily logging, +/// per say). +pub fn prometheus_metrics() -> warp::filters::log::Log<impl Fn(warp::filters::log::Info) + Clone> { + warp::log::custom(move |info| { + // Here we restrict the `info.path()` value to some predefined values. Without this, we end + // up with a new metric type each time someone includes something unique in the path (e.g., + // a block hash). + let path = { + let equals = |s: &'static str| -> Option<&'static str> { + if info.path() == format!("/{}/{}/{}", API_PREFIX, API_VERSION, s) { + Some(s) + } else { + None + } + }; + + let starts_with = |s: &'static str| -> Option<&'static str> { + if info + .path() + .starts_with(&format!("/{}/{}/{}", API_PREFIX, API_VERSION, s)) + { + Some(s) + } else { + None + } + }; + + equals("beacon/blocks") + .or_else(|| starts_with("validator/duties/attester")) + .or_else(|| starts_with("validator/duties/proposer")) + .or_else(|| starts_with("validator/attestation_data")) + .or_else(|| starts_with("validator/blocks")) + .or_else(|| starts_with("validator/aggregate_attestation")) + .or_else(|| starts_with("validator/aggregate_and_proofs")) + .or_else(|| starts_with("validator/beacon_committee_subscriptions")) + .or_else(|| starts_with("beacon/")) + .or_else(|| starts_with("config/")) + .or_else(|| starts_with("debug/")) + .or_else(|| starts_with("events/")) + .or_else(|| starts_with("node/")) + .or_else(|| starts_with("validator/")) + .unwrap_or("other") + }; + + metrics::inc_counter_vec(&metrics::HTTP_API_PATHS_TOTAL, &[path]); + metrics::inc_counter_vec( + &metrics::HTTP_API_STATUS_CODES_TOTAL, + &[&info.status().to_string()], + ); + metrics::observe_timer_vec(&metrics::HTTP_API_PATHS_TIMES, &[path], info.elapsed()); + }) +} + +/// Creates a server that will serve requests using information from `ctx`. +/// +/// The server will shut down gracefully when the `shutdown` future resolves. +/// +/// ## Returns +/// +/// This function will bind the server to the provided address and then return a tuple of: +/// +/// - `SocketAddr`: the address that the HTTP server will listen on. +/// - `Future`: the actual server future that will need to be awaited. +/// +/// ## Errors +/// +/// Returns an error if the server is unable to bind or there is another error during +/// configuration. +pub fn serve<T: BeaconChainTypes>( + ctx: Arc<Context<T>>, + shutdown: impl Future<Output = ()> + Send + Sync + 'static, +) -> Result<(SocketAddr, impl Future<Output = ()>), Error> { + let config = ctx.config.clone(); + let log = ctx.log.clone(); + let allow_origin = config.allow_origin.clone(); + + // Sanity check. + if !config.enabled { + crit!(log, "Cannot start disabled HTTP server"); + return Err(Error::Other( + "A disabled server should not be started".to_string(), + )); + } + + let eth1_v1 = warp::path(API_PREFIX).and(warp::path(API_VERSION)); + + // Instantiate the beacon proposer cache. + let beacon_proposer_cache = ctx + .chain + .as_ref() + .map(|chain| BeaconProposerCache::new(&chain)) + .transpose() + .map_err(|e| format!("Unable to initialize beacon proposer cache: {:?}", e))? + .map(Mutex::new) + .map(Arc::new); + + // Create a `warp` filter that provides access to the proposer cache. + let beacon_proposer_cache = || { + warp::any() + .map(move || beacon_proposer_cache.clone()) + .and_then(|beacon_proposer_cache| async move { + match beacon_proposer_cache { + Some(cache) => Ok(cache), + None => Err(warp_utils::reject::custom_not_found( + "Beacon proposer cache is not initialized.".to_string(), + )), + } + }) + }; + + // Create a `warp` filter that provides access to the network globals. + let inner_network_globals = ctx.network_globals.clone(); + let network_globals = warp::any() + .map(move || inner_network_globals.clone()) + .and_then(|network_globals| async move { + match network_globals { + Some(globals) => Ok(globals), + None => Err(warp_utils::reject::custom_not_found( + "network globals are not initialized.".to_string(), + )), + } + }); + + // Create a `warp` filter that provides access to the beacon chain. + let inner_ctx = ctx.clone(); + let chain_filter = + warp::any() + .map(move || inner_ctx.chain.clone()) + .and_then(|chain| async move { + match chain { + Some(chain) => Ok(chain), + None => Err(warp_utils::reject::custom_not_found( + "Beacon chain genesis has not yet been observed.".to_string(), + )), + } + }); + + // Create a `warp` filter that provides access to the network sender channel. + let inner_ctx = ctx.clone(); + let network_tx_filter = warp::any() + .map(move || inner_ctx.network_tx.clone()) + .and_then(|network_tx| async move { + match network_tx { + Some(network_tx) => Ok(network_tx), + None => Err(warp_utils::reject::custom_not_found( + "The networking stack has not yet started.".to_string(), + )), + } + }); + + // Create a `warp` filter that rejects request whilst the node is syncing. + let not_while_syncing_filter = warp::any() + .and(network_globals.clone()) + .and(chain_filter.clone()) + .and_then( + |network_globals: Arc<NetworkGlobals<T::EthSpec>>, chain: Arc<BeaconChain<T>>| async move { + match *network_globals.sync_state.read() { + SyncState::SyncingFinalized { head_slot, .. } => { + let current_slot = chain + .slot_clock + .now_or_genesis() + .ok_or_else(|| { + warp_utils::reject::custom_server_error( + "unable to read slot clock".to_string(), + ) + })?; + + let tolerance = SYNC_TOLERANCE_EPOCHS * T::EthSpec::slots_per_epoch(); + + if head_slot + tolerance >= current_slot { + Ok(()) + } else { + Err(warp_utils::reject::not_synced(format!( + "head slot is {}, current slot is {}", + head_slot, current_slot + ))) + } + } + SyncState::SyncingHead { .. } => Ok(()), + SyncState::Synced => Ok(()), + SyncState::Stalled => Err(warp_utils::reject::not_synced( + "sync is stalled".to_string(), + )), + } + }, + ) + .untuple_one(); + + // Create a `warp` filter that provides access to the logger. + let log_filter = warp::any().map(move || ctx.log.clone()); + + /* + * + * Start of HTTP method definitions. + * + */ + + // GET beacon/genesis + let get_beacon_genesis = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("genesis")) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + chain + .head_info() + .map_err(warp_utils::reject::beacon_chain_error) + .map(|head| api_types::GenesisData { + genesis_time: head.genesis_time, + genesis_validators_root: head.genesis_validators_root, + genesis_fork_version: chain.spec.genesis_fork_version, + }) + .map(api_types::GenericResponse::from) + }) + }); + + /* + * beacon/states/{state_id} + */ + + let beacon_states_path = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("states")) + .and(warp::path::param::<StateId>()) + .and(chain_filter.clone()); + + // GET beacon/states/{state_id}/root + let get_beacon_state_root = beacon_states_path + .clone() + .and(warp::path("root")) + .and(warp::path::end()) + .and_then(|state_id: StateId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + state_id + .root(&chain) + .map(api_types::RootData::from) + .map(api_types::GenericResponse::from) + }) + }); + + // GET beacon/states/{state_id}/fork + let get_beacon_state_fork = beacon_states_path + .clone() + .and(warp::path("fork")) + .and(warp::path::end()) + .and_then(|state_id: StateId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || state_id.fork(&chain).map(api_types::GenericResponse::from)) + }); + + // GET beacon/states/{state_id}/finality_checkpoints + let get_beacon_state_finality_checkpoints = beacon_states_path + .clone() + .and(warp::path("finality_checkpoints")) + .and(warp::path::end()) + .and_then(|state_id: StateId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + state_id + .map_state(&chain, |state| { + Ok(api_types::FinalityCheckpointsData { + previous_justified: state.previous_justified_checkpoint, + current_justified: state.current_justified_checkpoint, + finalized: state.finalized_checkpoint, + }) + }) + .map(api_types::GenericResponse::from) + }) + }); + + // GET beacon/states/{state_id}/validators + let get_beacon_state_validators = beacon_states_path + .clone() + .and(warp::path("validators")) + .and(warp::path::end()) + .and_then(|state_id: StateId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + state_id + .map_state(&chain, |state| { + let epoch = state.current_epoch(); + let finalized_epoch = state.finalized_checkpoint.epoch; + let far_future_epoch = chain.spec.far_future_epoch; + + Ok(state + .validators + .iter() + .zip(state.balances.iter()) + .enumerate() + .map(|(index, (validator, balance))| api_types::ValidatorData { + index: index as u64, + balance: *balance, + status: api_types::ValidatorStatus::from_validator( + Some(validator), + epoch, + finalized_epoch, + far_future_epoch, + ), + validator: validator.clone(), + }) + .collect::<Vec<_>>()) + }) + .map(api_types::GenericResponse::from) + }) + }); + + // GET beacon/states/{state_id}/validators/{validator_id} + let get_beacon_state_validators_id = beacon_states_path + .clone() + .and(warp::path("validators")) + .and(warp::path::param::<ValidatorId>()) + .and(warp::path::end()) + .and_then( + |state_id: StateId, chain: Arc<BeaconChain<T>>, validator_id: ValidatorId| { + blocking_json_task(move || { + state_id + .map_state(&chain, |state| { + let index_opt = match &validator_id { + ValidatorId::PublicKey(pubkey) => { + state.validators.iter().position(|v| v.pubkey == *pubkey) + } + ValidatorId::Index(index) => Some(*index as usize), + }; + + index_opt + .and_then(|index| { + let validator = state.validators.get(index)?; + let balance = *state.balances.get(index)?; + let epoch = state.current_epoch(); + let finalized_epoch = state.finalized_checkpoint.epoch; + let far_future_epoch = chain.spec.far_future_epoch; + + Some(api_types::ValidatorData { + index: index as u64, + balance, + status: api_types::ValidatorStatus::from_validator( + Some(validator), + epoch, + finalized_epoch, + far_future_epoch, + ), + validator: validator.clone(), + }) + }) + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "unknown validator: {}", + validator_id + )) + }) + }) + .map(api_types::GenericResponse::from) + }) + }, + ); + + // GET beacon/states/{state_id}/committees/{epoch} + let get_beacon_state_committees = beacon_states_path + .clone() + .and(warp::path("committees")) + .and(warp::path::param::<Epoch>()) + .and(warp::query::<api_types::CommitteesQuery>()) + .and(warp::path::end()) + .and_then( + |state_id: StateId, + chain: Arc<BeaconChain<T>>, + epoch: Epoch, + query: api_types::CommitteesQuery| { + blocking_json_task(move || { + state_id.map_state(&chain, |state| { + let relative_epoch = + RelativeEpoch::from_epoch(state.current_epoch(), epoch).map_err( + |_| { + warp_utils::reject::custom_bad_request(format!( + "state is epoch {} and only previous, current and next epochs are supported", + state.current_epoch() + )) + }, + )?; + + let committee_cache = if state + .committee_cache_is_initialized(relative_epoch) + { + state.committee_cache(relative_epoch).map(Cow::Borrowed) + } else { + CommitteeCache::initialized(state, epoch, &chain.spec).map(Cow::Owned) + } + .map_err(BeaconChainError::BeaconStateError) + .map_err(warp_utils::reject::beacon_chain_error)?; + + // Use either the supplied slot or all slots in the epoch. + let slots = query.slot.map(|slot| vec![slot]).unwrap_or_else(|| { + epoch.slot_iter(T::EthSpec::slots_per_epoch()).collect() + }); + + // Use either the supplied committee index or all available indices. + let indices = query.index.map(|index| vec![index]).unwrap_or_else(|| { + (0..committee_cache.committees_per_slot()).collect() + }); + + let mut response = Vec::with_capacity(slots.len() * indices.len()); + + for slot in slots { + // It is not acceptable to query with a slot that is not within the + // specified epoch. + if slot.epoch(T::EthSpec::slots_per_epoch()) != epoch { + return Err(warp_utils::reject::custom_bad_request(format!( + "{} is not in epoch {}", + slot, epoch + ))); + } + + for &index in &indices { + let committee = committee_cache + .get_beacon_committee(slot, index) + .ok_or_else(|| { + warp_utils::reject::custom_bad_request(format!( + "committee index {} does not exist in epoch {}", + index, epoch + )) + })?; + + response.push(api_types::CommitteeData { + index, + slot, + validators: committee + .committee + .iter() + .map(|i| *i as u64) + .collect(), + }); + } + } + + Ok(api_types::GenericResponse::from(response)) + }) + }) + }, + ); + + // GET beacon/headers + // + // Note: this endpoint only returns information about blocks in the canonical chain. Given that + // there's a `canonical` flag on the response, I assume it should also return non-canonical + // things. Returning non-canonical things is hard for us since we don't already have a + // mechanism for arbitrary forwards block iteration, we only support iterating forwards along + // the canonical chain. + let get_beacon_headers = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("headers")) + .and(warp::query::<api_types::HeadersQuery>()) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then( + |query: api_types::HeadersQuery, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let (root, block) = match (query.slot, query.parent_root) { + // No query parameters, return the canonical head block. + (None, None) => chain + .head_beacon_block() + .map_err(warp_utils::reject::beacon_chain_error) + .map(|block| (block.canonical_root(), block))?, + // Only the parent root parameter, do a forwards-iterator lookup. + (None, Some(parent_root)) => { + let parent = BlockId::from_root(parent_root).block(&chain)?; + let (root, _slot) = chain + .forwards_iter_block_roots(parent.slot()) + .map_err(warp_utils::reject::beacon_chain_error)? + // Ignore any skip-slots immediately following the parent. + .find(|res| { + res.as_ref().map_or(false, |(root, _)| *root != parent_root) + }) + .transpose() + .map_err(warp_utils::reject::beacon_chain_error)? + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "child of block with root {}", + parent_root + )) + })?; + + BlockId::from_root(root) + .block(&chain) + .map(|block| (root, block))? + } + // Slot is supplied, search by slot and optionally filter by + // parent root. + (Some(slot), parent_root_opt) => { + let root = BlockId::from_slot(slot).root(&chain)?; + let block = BlockId::from_root(root).block(&chain)?; + + // If the parent root was supplied, check that it matches the block + // obtained via a slot lookup. + if let Some(parent_root) = parent_root_opt { + if block.parent_root() != parent_root { + return Err(warp_utils::reject::custom_not_found(format!( + "no canonical block at slot {} with parent root {}", + slot, parent_root + ))); + } + } + + (root, block) + } + }; + + let data = api_types::BlockHeaderData { + root, + canonical: true, + header: api_types::BlockHeaderAndSignature { + message: block.message.block_header(), + signature: block.signature.into(), + }, + }; + + Ok(api_types::GenericResponse::from(vec![data])) + }) + }, + ); + + // GET beacon/headers/{block_id} + let get_beacon_headers_block_id = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("headers")) + .and(warp::path::param::<BlockId>()) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|block_id: BlockId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let root = block_id.root(&chain)?; + let block = BlockId::from_root(root).block(&chain)?; + + let canonical = chain + .block_root_at_slot(block.slot()) + .map_err(warp_utils::reject::beacon_chain_error)? + .map_or(false, |canonical| root == canonical); + + let data = api_types::BlockHeaderData { + root, + canonical, + header: api_types::BlockHeaderAndSignature { + message: block.message.block_header(), + signature: block.signature.into(), + }, + }; + + Ok(api_types::GenericResponse::from(data)) + }) + }); + + /* + * beacon/blocks + */ + + // POST beacon/blocks/{block_id} + let post_beacon_blocks = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .and_then( + |block: SignedBeaconBlock<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>, + log: Logger| { + blocking_json_task(move || { + // Send the block, regardless of whether or not it is valid. The API + // specification is very clear that this is the desired behaviour. + publish_pubsub_message( + &network_tx, + PubsubMessage::BeaconBlock(Box::new(block.clone())), + )?; + + match chain.process_block(block.clone()) { + Ok(root) => { + info!( + log, + "Valid block from HTTP API"; + "root" => format!("{}", root) + ); + + // Update the head since it's likely this block will become the new + // head. + chain + .fork_choice() + .map_err(warp_utils::reject::beacon_chain_error)?; + + Ok(()) + } + Err(e) => { + let msg = format!("{:?}", e); + error!( + log, + "Invalid block provided to HTTP API"; + "reason" => &msg + ); + Err(warp_utils::reject::broadcast_without_import(msg)) + } + } + }) + }, + ); + + let beacon_blocks_path = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(warp::path::param::<BlockId>()) + .and(chain_filter.clone()); + + // GET beacon/blocks/{block_id} + let get_beacon_block = beacon_blocks_path.clone().and(warp::path::end()).and_then( + |block_id: BlockId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || block_id.block(&chain).map(api_types::GenericResponse::from)) + }, + ); + + // GET beacon/blocks/{block_id}/root + let get_beacon_block_root = beacon_blocks_path + .clone() + .and(warp::path("root")) + .and(warp::path::end()) + .and_then(|block_id: BlockId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + block_id + .root(&chain) + .map(api_types::RootData::from) + .map(api_types::GenericResponse::from) + }) + }); + + // GET beacon/blocks/{block_id}/attestations + let get_beacon_block_attestations = beacon_blocks_path + .clone() + .and(warp::path("attestations")) + .and(warp::path::end()) + .and_then(|block_id: BlockId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + block_id + .block(&chain) + .map(|block| block.message.body.attestations) + .map(api_types::GenericResponse::from) + }) + }); + + /* + * beacon/pool + */ + + let beacon_pool_path = eth1_v1 + .and(warp::path("beacon")) + .and(warp::path("pool")) + .and(chain_filter.clone()); + + // POST beacon/pool/attestations + let post_beacon_pool_attestations = beacon_pool_path + .clone() + .and(warp::path("attestations")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and_then( + |chain: Arc<BeaconChain<T>>, + attestation: Attestation<T::EthSpec>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + blocking_json_task(move || { + let attestation = chain + .verify_unaggregated_attestation_for_gossip(attestation.clone(), None) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + publish_pubsub_message( + &network_tx, + PubsubMessage::Attestation(Box::new(( + attestation.subnet_id(), + attestation.attestation().clone(), + ))), + )?; + + chain + .apply_attestation_to_fork_choice(&attestation) + .map_err(|e| { + warp_utils::reject::broadcast_without_import(format!( + "not applied to fork choice: {:?}", + e + )) + })?; + + chain + .add_to_naive_aggregation_pool(attestation) + .map_err(|e| { + warp_utils::reject::broadcast_without_import(format!( + "not applied to naive aggregation pool: {:?}", + e + )) + })?; + + Ok(()) + }) + }, + ); + + // GET beacon/pool/attestations + let get_beacon_pool_attestations = beacon_pool_path + .clone() + .and(warp::path("attestations")) + .and(warp::path::end()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let mut attestations = chain.op_pool.get_all_attestations(); + attestations.extend(chain.naive_aggregation_pool.read().iter().cloned()); + Ok(api_types::GenericResponse::from(attestations)) + }) + }); + + // POST beacon/pool/attester_slashings + let post_beacon_pool_attester_slashings = beacon_pool_path + .clone() + .and(warp::path("attester_slashings")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and_then( + |chain: Arc<BeaconChain<T>>, + slashing: AttesterSlashing<T::EthSpec>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + blocking_json_task(move || { + let outcome = chain + .verify_attester_slashing_for_gossip(slashing.clone()) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + if let ObservationOutcome::New(slashing) = outcome { + publish_pubsub_message( + &network_tx, + PubsubMessage::AttesterSlashing(Box::new( + slashing.clone().into_inner(), + )), + )?; + + chain + .import_attester_slashing(slashing) + .map_err(warp_utils::reject::beacon_chain_error)?; + } + + Ok(()) + }) + }, + ); + + // GET beacon/pool/attester_slashings + let get_beacon_pool_attester_slashings = beacon_pool_path + .clone() + .and(warp::path("attester_slashings")) + .and(warp::path::end()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let attestations = chain.op_pool.get_all_attester_slashings(); + Ok(api_types::GenericResponse::from(attestations)) + }) + }); + + // POST beacon/pool/proposer_slashings + let post_beacon_pool_proposer_slashings = beacon_pool_path + .clone() + .and(warp::path("proposer_slashings")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and_then( + |chain: Arc<BeaconChain<T>>, + slashing: ProposerSlashing, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + blocking_json_task(move || { + let outcome = chain + .verify_proposer_slashing_for_gossip(slashing.clone()) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + if let ObservationOutcome::New(slashing) = outcome { + publish_pubsub_message( + &network_tx, + PubsubMessage::ProposerSlashing(Box::new( + slashing.clone().into_inner(), + )), + )?; + + chain.import_proposer_slashing(slashing); + } + + Ok(()) + }) + }, + ); + + // GET beacon/pool/proposer_slashings + let get_beacon_pool_proposer_slashings = beacon_pool_path + .clone() + .and(warp::path("proposer_slashings")) + .and(warp::path::end()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let attestations = chain.op_pool.get_all_proposer_slashings(); + Ok(api_types::GenericResponse::from(attestations)) + }) + }); + + // POST beacon/pool/voluntary_exits + let post_beacon_pool_voluntary_exits = beacon_pool_path + .clone() + .and(warp::path("voluntary_exits")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and_then( + |chain: Arc<BeaconChain<T>>, + exit: SignedVoluntaryExit, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + blocking_json_task(move || { + let outcome = chain + .verify_voluntary_exit_for_gossip(exit.clone()) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + if let ObservationOutcome::New(exit) = outcome { + publish_pubsub_message( + &network_tx, + PubsubMessage::VoluntaryExit(Box::new(exit.clone().into_inner())), + )?; + + chain.import_voluntary_exit(exit); + } + + Ok(()) + }) + }, + ); + + // GET beacon/pool/voluntary_exits + let get_beacon_pool_voluntary_exits = beacon_pool_path + .clone() + .and(warp::path("voluntary_exits")) + .and(warp::path::end()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let attestations = chain.op_pool.get_all_voluntary_exits(); + Ok(api_types::GenericResponse::from(attestations)) + }) + }); + + /* + * config/fork_schedule + */ + + let config_path = eth1_v1.and(warp::path("config")); + + // GET config/fork_schedule + let get_config_fork_schedule = config_path + .clone() + .and(warp::path("fork_schedule")) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + StateId::head() + .fork(&chain) + .map(|fork| api_types::GenericResponse::from(vec![fork])) + }) + }); + + // GET config/spec + let get_config_spec = config_path + .clone() + .and(warp::path("spec")) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + Ok(api_types::GenericResponse::from(YamlConfig::from_spec::< + T::EthSpec, + >( + &chain.spec + ))) + }) + }); + + // GET config/deposit_contract + let get_config_deposit_contract = config_path + .clone() + .and(warp::path("deposit_contract")) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + Ok(api_types::GenericResponse::from( + api_types::DepositContractData { + address: chain.spec.deposit_contract_address, + chain_id: eth1::DEFAULT_NETWORK_ID.into(), + }, + )) + }) + }); + + /* + * debug + */ + + // GET debug/beacon/states/{state_id} + let get_debug_beacon_states = eth1_v1 + .and(warp::path("debug")) + .and(warp::path("beacon")) + .and(warp::path("states")) + .and(warp::path::param::<StateId>()) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|state_id: StateId, chain: Arc<BeaconChain<T>>| { + blocking_task(move || { + state_id.map_state(&chain, |state| { + Ok(warp::reply::json(&api_types::GenericResponseRef::from( + &state, + ))) + }) + }) + }); + + // GET debug/beacon/heads + let get_debug_beacon_heads = eth1_v1 + .and(warp::path("debug")) + .and(warp::path("beacon")) + .and(warp::path("heads")) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let heads = chain + .heads() + .into_iter() + .map(|(root, slot)| api_types::ChainHeadData { root, slot }) + .collect::<Vec<_>>(); + Ok(api_types::GenericResponse::from(heads)) + }) + }); + + /* + * node + */ + + // GET node/identity + let get_node_identity = eth1_v1 + .and(warp::path("node")) + .and(warp::path("identity")) + .and(warp::path::end()) + .and(network_globals.clone()) + .and_then(|network_globals: Arc<NetworkGlobals<T::EthSpec>>| { + blocking_json_task(move || { + Ok(api_types::GenericResponse::from(api_types::IdentityData { + peer_id: network_globals.local_peer_id().to_base58(), + enr: network_globals.local_enr(), + p2p_addresses: network_globals.listen_multiaddrs(), + })) + }) + }); + + // GET node/version + let get_node_version = eth1_v1 + .and(warp::path("node")) + .and(warp::path("version")) + .and(warp::path::end()) + .and_then(|| { + blocking_json_task(move || { + Ok(api_types::GenericResponse::from(api_types::VersionData { + version: version_with_platform(), + })) + }) + }); + + // GET node/syncing + let get_node_syncing = eth1_v1 + .and(warp::path("node")) + .and(warp::path("syncing")) + .and(warp::path::end()) + .and(network_globals.clone()) + .and(chain_filter.clone()) + .and_then( + |network_globals: Arc<NetworkGlobals<T::EthSpec>>, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let head_slot = chain + .head_info() + .map(|info| info.slot) + .map_err(warp_utils::reject::beacon_chain_error)?; + let current_slot = chain + .slot() + .map_err(warp_utils::reject::beacon_chain_error)?; + + // Taking advantage of saturating subtraction on slot. + let sync_distance = current_slot - head_slot; + + let syncing_data = api_types::SyncingData { + is_syncing: network_globals.sync_state.read().is_syncing(), + head_slot, + sync_distance, + }; + + Ok(api_types::GenericResponse::from(syncing_data)) + }) + }, + ); + + /* + * validator + */ + + // GET validator/duties/attester/{epoch} + let get_validator_duties_attester = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("attester")) + .and(warp::path::param::<Epoch>()) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(warp::query::<api_types::ValidatorDutiesQuery>()) + .and(chain_filter.clone()) + .and_then( + |epoch: Epoch, query: api_types::ValidatorDutiesQuery, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let current_epoch = chain + .epoch() + .map_err(warp_utils::reject::beacon_chain_error)?; + + if epoch > current_epoch + 1 { + return Err(warp_utils::reject::custom_bad_request(format!( + "request epoch {} is more than one epoch past the current epoch {}", + epoch, current_epoch + ))); + } + + let validator_count = StateId::head() + .map_state(&chain, |state| Ok(state.validators.len() as u64))?; + + let indices = query + .index + .as_ref() + .map(|index| index.0.clone()) + .map(Result::Ok) + .unwrap_or_else(|| { + Ok::<_, warp::Rejection>((0..validator_count).collect()) + })?; + + let pubkeys = indices + .into_iter() + .filter(|i| *i < validator_count as u64) + .map(|i| { + let pubkey = chain + .validator_pubkey(i as usize) + .map_err(warp_utils::reject::beacon_chain_error)? + .ok_or_else(|| { + warp_utils::reject::custom_bad_request(format!( + "unknown validator index {}", + i + )) + })?; + + Ok((i, pubkey)) + }) + .collect::<Result<Vec<_>, warp::Rejection>>()?; + + // Converts the internal Lighthouse `AttestationDuty` struct into an + // API-conforming `AttesterData` struct. + let convert = |validator_index: u64, + pubkey: PublicKey, + duty: AttestationDuty| + -> api_types::AttesterData { + api_types::AttesterData { + pubkey: pubkey.into(), + validator_index, + committees_at_slot: duty.committees_at_slot, + committee_index: duty.index, + committee_length: duty.committee_len as u64, + validator_committee_index: duty.committee_position as u64, + slot: duty.slot, + } + }; + + // Here we have two paths: + // + // ## Fast + // + // If the request epoch is the current epoch, use the cached beacon chain + // method. + // + // ## Slow + // + // If the request epoch is prior to the current epoch, load a beacon state from + // disk + // + // The idea is to stop historical requests from washing out the cache on the + // beacon chain, whilst allowing a VC to request duties quickly. + let duties = if epoch == current_epoch { + // Fast path. + pubkeys + .into_iter() + // Exclude indices which do not represent a known public key and a + // validator duty. + .filter_map(|(i, pubkey)| { + Some( + chain + .validator_attestation_duty(i as usize, epoch) + .transpose()? + .map_err(warp_utils::reject::beacon_chain_error) + .map(|duty| convert(i, pubkey, duty)), + ) + }) + .collect::<Result<Vec<_>, warp::Rejection>>()? + } else { + // If the head state is equal to or earlier than the request epoch, use it. + let mut state = chain + .with_head(|head| { + if head.beacon_state.current_epoch() <= epoch { + Ok(Some( + head.beacon_state + .clone_with(CloneConfig::committee_caches_only()), + )) + } else { + Ok(None) + } + }) + .map_err(warp_utils::reject::beacon_chain_error)? + .map(Result::Ok) + .unwrap_or_else(|| { + StateId::slot(epoch.start_slot(T::EthSpec::slots_per_epoch())) + .state(&chain) + })?; + + // Only skip forward to the epoch prior to the request, since we have a + // one-epoch look-ahead on shuffling. + while state + .next_epoch() + .map_err(warp_utils::reject::beacon_state_error)? + < epoch + { + // Don't calculate state roots since they aren't required for calculating + // shuffling (achieved by providing Hash256::zero()). + per_slot_processing(&mut state, Some(Hash256::zero()), &chain.spec) + .map_err(warp_utils::reject::slot_processing_error)?; + } + + let relative_epoch = + RelativeEpoch::from_epoch(state.current_epoch(), epoch).map_err( + |e| { + warp_utils::reject::custom_server_error(format!( + "unable to obtain suitable state: {:?}", + e + )) + }, + )?; + + state + .build_committee_cache(relative_epoch, &chain.spec) + .map_err(warp_utils::reject::beacon_state_error)?; + pubkeys + .into_iter() + .filter_map(|(i, pubkey)| { + Some( + state + .get_attestation_duties(i as usize, relative_epoch) + .transpose()? + .map_err(warp_utils::reject::beacon_state_error) + .map(|duty| convert(i, pubkey, duty)), + ) + }) + .collect::<Result<Vec<_>, warp::Rejection>>()? + }; + + Ok(api_types::GenericResponse::from(duties)) + }) + }, + ); + + // GET validator/duties/proposer/{epoch} + let get_validator_duties_proposer = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("proposer")) + .and(warp::path::param::<Epoch>()) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(chain_filter.clone()) + .and(beacon_proposer_cache()) + .and_then( + |epoch: Epoch, + chain: Arc<BeaconChain<T>>, + beacon_proposer_cache: Arc<Mutex<BeaconProposerCache>>| { + blocking_json_task(move || { + beacon_proposer_cache + .lock() + .get_proposers(&chain, epoch) + .map(api_types::GenericResponse::from) + }) + }, + ); + + // GET validator/blocks/{slot} + let get_validator_blocks = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("blocks")) + .and(warp::path::param::<Slot>()) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(warp::query::<api_types::ValidatorBlocksQuery>()) + .and(chain_filter.clone()) + .and_then( + |slot: Slot, query: api_types::ValidatorBlocksQuery, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + let randao_reveal = (&query.randao_reveal).try_into().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not valid BLS signature: {:?}", + e + )) + })?; + + chain + .produce_block(randao_reveal, slot, query.graffiti.map(Into::into)) + .map(|block_and_state| block_and_state.0) + .map(api_types::GenericResponse::from) + .map_err(warp_utils::reject::block_production_error) + }) + }, + ); + + // GET validator/attestation_data?slot,committee_index + let get_validator_attestation_data = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("attestation_data")) + .and(warp::path::end()) + .and(warp::query::<api_types::ValidatorAttestationDataQuery>()) + .and(not_while_syncing_filter.clone()) + .and(chain_filter.clone()) + .and_then( + |query: api_types::ValidatorAttestationDataQuery, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + chain + .produce_unaggregated_attestation(query.slot, query.committee_index) + .map(|attestation| attestation.data) + .map(api_types::GenericResponse::from) + .map_err(warp_utils::reject::beacon_chain_error) + }) + }, + ); + + // GET validator/aggregate_attestation?attestation_data_root,slot + let get_validator_aggregate_attestation = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("aggregate_attestation")) + .and(warp::path::end()) + .and(warp::query::<api_types::ValidatorAggregateAttestationQuery>()) + .and(not_while_syncing_filter.clone()) + .and(chain_filter.clone()) + .and_then( + |query: api_types::ValidatorAggregateAttestationQuery, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + chain + .get_aggregated_attestation_by_slot_and_root( + query.slot, + &query.attestation_data_root, + ) + .map(api_types::GenericResponse::from) + .ok_or_else(|| { + warp_utils::reject::custom_not_found( + "no matching aggregate found".to_string(), + ) + }) + }) + }, + ); + + // POST validator/aggregate_and_proofs + let post_validator_aggregate_and_proofs = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("aggregate_and_proofs")) + .and(warp::path::end()) + .and(not_while_syncing_filter) + .and(chain_filter.clone()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and_then( + |chain: Arc<BeaconChain<T>>, + aggregate: SignedAggregateAndProof<T::EthSpec>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + blocking_json_task(move || { + let aggregate = + match chain.verify_aggregated_attestation_for_gossip(aggregate.clone()) { + Ok(aggregate) => aggregate, + // If we already know the attestation, don't broadcast it or attempt to + // further verify it. Return success. + // + // It's reasonably likely that two different validators produce + // identical aggregates, especially if they're using the same beacon + // node. + Err(AttnError::AttestationAlreadyKnown(_)) => return Ok(()), + Err(e) => { + return Err(warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + ))) + } + }; + + publish_pubsub_message( + &network_tx, + PubsubMessage::AggregateAndProofAttestation(Box::new( + aggregate.aggregate().clone(), + )), + )?; + + chain + .apply_attestation_to_fork_choice(&aggregate) + .map_err(|e| { + warp_utils::reject::broadcast_without_import(format!( + "not applied to fork choice: {:?}", + e + )) + })?; + + chain.add_to_block_inclusion_pool(aggregate).map_err(|e| { + warp_utils::reject::broadcast_without_import(format!( + "not applied to block inclusion pool: {:?}", + e + )) + })?; + + Ok(()) + }) + }, + ); + + // POST validator/beacon_committee_subscriptions + let post_validator_beacon_committee_subscriptions = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("beacon_committee_subscriptions")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter) + .and_then( + |subscriptions: Vec<api_types::BeaconCommitteeSubscription>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + blocking_json_task(move || { + for subscription in &subscriptions { + let subscription = api_types::ValidatorSubscription { + validator_index: subscription.validator_index, + attestation_committee_index: subscription.committee_index, + slot: subscription.slot, + committee_count_at_slot: subscription.committees_at_slot, + is_aggregator: subscription.is_aggregator, + }; + + publish_network_message( + &network_tx, + NetworkMessage::Subscribe { + subscriptions: vec![subscription], + }, + )?; + } + + Ok(()) + }) + }, + ); + + // GET lighthouse/health + let get_lighthouse_health = warp::path("lighthouse") + .and(warp::path("health")) + .and(warp::path::end()) + .and_then(|| { + blocking_json_task(move || { + eth2::lighthouse::Health::observe() + .map(api_types::GenericResponse::from) + .map_err(warp_utils::reject::custom_bad_request) + }) + }); + + // GET lighthouse/syncing + let get_lighthouse_syncing = warp::path("lighthouse") + .and(warp::path("syncing")) + .and(warp::path::end()) + .and(network_globals.clone()) + .and_then(|network_globals: Arc<NetworkGlobals<T::EthSpec>>| { + blocking_json_task(move || { + Ok(api_types::GenericResponse::from( + network_globals.sync_state(), + )) + }) + }); + + // GET lighthouse/peers + let get_lighthouse_peers = warp::path("lighthouse") + .and(warp::path("peers")) + .and(warp::path::end()) + .and(network_globals.clone()) + .and_then(|network_globals: Arc<NetworkGlobals<T::EthSpec>>| { + blocking_json_task(move || { + Ok(network_globals + .peers + .read() + .peers() + .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { + peer_id: peer_id.to_string(), + peer_info: peer_info.clone(), + }) + .collect::<Vec<_>>()) + }) + }); + + // GET lighthouse/peers/connected + let get_lighthouse_peers_connected = warp::path("lighthouse") + .and(warp::path("peers")) + .and(warp::path("connected")) + .and(warp::path::end()) + .and(network_globals) + .and_then(|network_globals: Arc<NetworkGlobals<T::EthSpec>>| { + blocking_json_task(move || { + Ok(network_globals + .peers + .read() + .connected_peers() + .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { + peer_id: peer_id.to_string(), + peer_info: peer_info.clone(), + }) + .collect::<Vec<_>>()) + }) + }); + + // GET lighthouse/proto_array + let get_lighthouse_proto_array = warp::path("lighthouse") + .and(warp::path("proto_array")) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|chain: Arc<BeaconChain<T>>| { + blocking_task(move || { + Ok::<_, warp::Rejection>(warp::reply::json(&api_types::GenericResponseRef::from( + chain.fork_choice.read().proto_array().core_proto_array(), + ))) + }) + }); + + // GET lighthouse/validator_inclusion/{epoch}/{validator_id} + let get_lighthouse_validator_inclusion_global = warp::path("lighthouse") + .and(warp::path("validator_inclusion")) + .and(warp::path::param::<Epoch>()) + .and(warp::path::param::<ValidatorId>()) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then( + |epoch: Epoch, validator_id: ValidatorId, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + validator_inclusion::validator_inclusion_data(epoch, &validator_id, &chain) + .map(api_types::GenericResponse::from) + }) + }, + ); + + // GET lighthouse/validator_inclusion/{epoch}/global + let get_lighthouse_validator_inclusion = warp::path("lighthouse") + .and(warp::path("validator_inclusion")) + .and(warp::path::param::<Epoch>()) + .and(warp::path("global")) + .and(warp::path::end()) + .and(chain_filter) + .and_then(|epoch: Epoch, chain: Arc<BeaconChain<T>>| { + blocking_json_task(move || { + validator_inclusion::global_validator_inclusion_data(epoch, &chain) + .map(api_types::GenericResponse::from) + }) + }); + + // Define the ultimate set of routes that will be provided to the server. + let routes = warp::get() + .and( + get_beacon_genesis + .or(get_beacon_state_root.boxed()) + .or(get_beacon_state_fork.boxed()) + .or(get_beacon_state_finality_checkpoints.boxed()) + .or(get_beacon_state_validators.boxed()) + .or(get_beacon_state_validators_id.boxed()) + .or(get_beacon_state_committees.boxed()) + .or(get_beacon_headers.boxed()) + .or(get_beacon_headers_block_id.boxed()) + .or(get_beacon_block.boxed()) + .or(get_beacon_block_attestations.boxed()) + .or(get_beacon_block_root.boxed()) + .or(get_beacon_pool_attestations.boxed()) + .or(get_beacon_pool_attester_slashings.boxed()) + .or(get_beacon_pool_proposer_slashings.boxed()) + .or(get_beacon_pool_voluntary_exits.boxed()) + .or(get_config_fork_schedule.boxed()) + .or(get_config_spec.boxed()) + .or(get_config_deposit_contract.boxed()) + .or(get_debug_beacon_states.boxed()) + .or(get_debug_beacon_heads.boxed()) + .or(get_node_identity.boxed()) + .or(get_node_version.boxed()) + .or(get_node_syncing.boxed()) + .or(get_validator_duties_attester.boxed()) + .or(get_validator_duties_proposer.boxed()) + .or(get_validator_blocks.boxed()) + .or(get_validator_attestation_data.boxed()) + .or(get_validator_aggregate_attestation.boxed()) + .or(get_lighthouse_health.boxed()) + .or(get_lighthouse_syncing.boxed()) + .or(get_lighthouse_peers.boxed()) + .or(get_lighthouse_peers_connected.boxed()) + .or(get_lighthouse_proto_array.boxed()) + .or(get_lighthouse_validator_inclusion_global.boxed()) + .or(get_lighthouse_validator_inclusion.boxed()) + .boxed(), + ) + .or(warp::post() + .and( + post_beacon_blocks + .or(post_beacon_pool_attestations.boxed()) + .or(post_beacon_pool_attester_slashings.boxed()) + .or(post_beacon_pool_proposer_slashings.boxed()) + .or(post_beacon_pool_voluntary_exits.boxed()) + .or(post_validator_aggregate_and_proofs.boxed()) + .or(post_validator_beacon_committee_subscriptions.boxed()) + .boxed(), + ) + .boxed()) + .boxed() + // Maps errors into HTTP responses. + .recover(warp_utils::reject::handle_rejection) + .with(slog_logging(log.clone())) + .with(prometheus_metrics()) + // Add a `Server` header. + .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) + // Maybe add some CORS headers. + .map(move |reply| warp_utils::reply::maybe_cors(reply, allow_origin.as_ref())); + + let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( + SocketAddrV4::new(config.listen_addr, config.listen_port), + async { + shutdown.await; + }, + )?; + + info!( + log, + "HTTP API started"; + "listen_address" => listening_socket.to_string(), + ); + + Ok((listening_socket, server)) +} + +/// Publish a message to the libp2p pubsub network. +fn publish_pubsub_message<T: EthSpec>( + network_tx: &UnboundedSender<NetworkMessage<T>>, + message: PubsubMessage<T>, +) -> Result<(), warp::Rejection> { + publish_network_message( + network_tx, + NetworkMessage::Publish { + messages: vec![message], + }, + ) +} + +/// Publish a message to the libp2p network. +fn publish_network_message<T: EthSpec>( + network_tx: &UnboundedSender<NetworkMessage<T>>, + message: NetworkMessage<T>, +) -> Result<(), warp::Rejection> { + network_tx.send(message).map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "unable to publish to network channel: {}", + e + )) + }) +} + +/// Execute some task in a tokio "blocking thread". These threads are ideal for long-running +/// (blocking) tasks since they don't jam up the core executor. +async fn blocking_task<F, T>(func: F) -> T +where + F: Fn() -> T, +{ + tokio::task::block_in_place(func) +} + +/// A convenience wrapper around `blocking_task` for use with `warp` JSON responses. +async fn blocking_json_task<F, T>(func: F) -> Result<warp::reply::Json, warp::Rejection> +where + F: Fn() -> Result<T, warp::Rejection>, + T: Serialize, +{ + blocking_task(func) + .await + .map(|resp| warp::reply::json(&resp)) +} diff --git a/beacon_node/http_api/src/metrics.rs b/beacon_node/http_api/src/metrics.rs new file mode 100644 index 000000000..c641df6a4 --- /dev/null +++ b/beacon_node/http_api/src/metrics.rs @@ -0,0 +1,32 @@ +pub use lighthouse_metrics::*; + +lazy_static::lazy_static! { + pub static ref HTTP_API_PATHS_TOTAL: Result<IntCounterVec> = try_create_int_counter_vec( + "http_api_paths_total", + "Count of HTTP requests received", + &["path"] + ); + pub static ref HTTP_API_STATUS_CODES_TOTAL: Result<IntCounterVec> = try_create_int_counter_vec( + "http_api_status_codes_total", + "Count of HTTP status codes returned", + &["status"] + ); + pub static ref HTTP_API_PATHS_TIMES: Result<HistogramVec> = try_create_histogram_vec( + "http_api_paths_times", + "Duration to process HTTP requests per path", + &["path"] + ); + + pub static ref HTTP_API_BEACON_PROPOSER_CACHE_TIMES: Result<Histogram> = try_create_histogram( + "http_api_beacon_proposer_cache_build_times", + "Duration to process HTTP requests per path", + ); + pub static ref HTTP_API_BEACON_PROPOSER_CACHE_HITS_TOTAL: Result<IntCounter> = try_create_int_counter( + "http_api_beacon_proposer_cache_hits_total", + "Count of times the proposer cache has been hit", + ); + pub static ref HTTP_API_BEACON_PROPOSER_CACHE_MISSES_TOTAL: Result<IntCounter> = try_create_int_counter( + "http_api_beacon_proposer_cache_misses_total", + "Count of times the proposer cache has been missed", + ); +} diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs new file mode 100644 index 000000000..11800648f --- /dev/null +++ b/beacon_node/http_api/src/state_id.rs @@ -0,0 +1,118 @@ +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::StateId as CoreStateId; +use std::str::FromStr; +use types::{BeaconState, EthSpec, Fork, Hash256, Slot}; + +/// Wraps `eth2::types::StateId` and provides common state-access functionality. E.g., reading +/// states or parts of states from the database. +pub struct StateId(CoreStateId); + +impl StateId { + pub fn head() -> Self { + Self(CoreStateId::Head) + } + + pub fn slot(slot: Slot) -> Self { + Self(CoreStateId::Slot(slot)) + } + + /// Return the state root identified by `self`. + pub fn root<T: BeaconChainTypes>( + &self, + chain: &BeaconChain<T>, + ) -> Result<Hash256, warp::Rejection> { + let slot = match &self.0 { + CoreStateId::Head => { + return chain + .head_info() + .map(|head| head.state_root) + .map_err(warp_utils::reject::beacon_chain_error) + } + CoreStateId::Genesis => return Ok(chain.genesis_state_root), + CoreStateId::Finalized => chain.head_info().map(|head| { + head.finalized_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()) + }), + CoreStateId::Justified => chain.head_info().map(|head| { + head.current_justified_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()) + }), + CoreStateId::Slot(slot) => Ok(*slot), + CoreStateId::Root(root) => return Ok(*root), + } + .map_err(warp_utils::reject::beacon_chain_error)?; + + chain + .state_root_at_slot(slot) + .map_err(warp_utils::reject::beacon_chain_error)? + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!("beacon state at slot {}", slot)) + }) + } + + /// Return the `fork` field of the state identified by `self`. + pub fn fork<T: BeaconChainTypes>( + &self, + chain: &BeaconChain<T>, + ) -> Result<Fork, warp::Rejection> { + self.map_state(chain, |state| Ok(state.fork)) + } + + /// Return the `BeaconState` identified by `self`. + pub fn state<T: BeaconChainTypes>( + &self, + chain: &BeaconChain<T>, + ) -> Result<BeaconState<T::EthSpec>, warp::Rejection> { + let (state_root, slot_opt) = match &self.0 { + CoreStateId::Head => { + return chain + .head_beacon_state() + .map_err(warp_utils::reject::beacon_chain_error) + } + CoreStateId::Slot(slot) => (self.root(chain)?, Some(*slot)), + _ => (self.root(chain)?, None), + }; + + chain + .get_state(&state_root, slot_opt) + .map_err(warp_utils::reject::beacon_chain_error) + .and_then(|opt| { + opt.ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon state at root {}", + state_root + )) + }) + }) + } + + /// Map a function across the `BeaconState` identified by `self`. + /// + /// This function will avoid instantiating/copying a new state when `self` points to the head + /// of the chain. + pub fn map_state<T: BeaconChainTypes, F, U>( + &self, + chain: &BeaconChain<T>, + func: F, + ) -> Result<U, warp::Rejection> + where + F: Fn(&BeaconState<T::EthSpec>) -> Result<U, warp::Rejection>, + { + match &self.0 { + CoreStateId::Head => chain + .with_head(|snapshot| Ok(func(&snapshot.beacon_state))) + .map_err(warp_utils::reject::beacon_chain_error)?, + _ => func(&self.state(chain)?), + } + } +} + +impl FromStr for StateId { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + CoreStateId::from_str(s).map(Self) + } +} diff --git a/beacon_node/http_api/src/validator_inclusion.rs b/beacon_node/http_api/src/validator_inclusion.rs new file mode 100644 index 000000000..90847dd6b --- /dev/null +++ b/beacon_node/http_api/src/validator_inclusion.rs @@ -0,0 +1,88 @@ +use crate::state_id::StateId; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::{ + lighthouse::{GlobalValidatorInclusionData, ValidatorInclusionData}, + types::ValidatorId, +}; +use state_processing::per_epoch_processing::ValidatorStatuses; +use types::{Epoch, EthSpec}; + +/// Returns information about *all validators* (i.e., global) and how they performed during a given +/// epoch. +pub fn global_validator_inclusion_data<T: BeaconChainTypes>( + epoch: Epoch, + chain: &BeaconChain<T>, +) -> Result<GlobalValidatorInclusionData, warp::Rejection> { + let target_slot = epoch.end_slot(T::EthSpec::slots_per_epoch()); + + let state = StateId::slot(target_slot).state(chain)?; + + let mut validator_statuses = ValidatorStatuses::new(&state, &chain.spec) + .map_err(warp_utils::reject::beacon_state_error)?; + validator_statuses + .process_attestations(&state, &chain.spec) + .map_err(warp_utils::reject::beacon_state_error)?; + + let totals = validator_statuses.total_balances; + + Ok(GlobalValidatorInclusionData { + current_epoch_active_gwei: totals.current_epoch(), + previous_epoch_active_gwei: totals.previous_epoch(), + current_epoch_attesting_gwei: totals.current_epoch_attesters(), + current_epoch_target_attesting_gwei: totals.current_epoch_target_attesters(), + previous_epoch_attesting_gwei: totals.previous_epoch_attesters(), + previous_epoch_target_attesting_gwei: totals.previous_epoch_target_attesters(), + previous_epoch_head_attesting_gwei: totals.previous_epoch_head_attesters(), + }) +} + +/// Returns information about a single validator and how it performed during a given epoch. +pub fn validator_inclusion_data<T: BeaconChainTypes>( + epoch: Epoch, + validator_id: &ValidatorId, + chain: &BeaconChain<T>, +) -> Result<Option<ValidatorInclusionData>, warp::Rejection> { + let target_slot = epoch.end_slot(T::EthSpec::slots_per_epoch()); + + let mut state = StateId::slot(target_slot).state(chain)?; + + let mut validator_statuses = ValidatorStatuses::new(&state, &chain.spec) + .map_err(warp_utils::reject::beacon_state_error)?; + validator_statuses + .process_attestations(&state, &chain.spec) + .map_err(warp_utils::reject::beacon_state_error)?; + + state + .update_pubkey_cache() + .map_err(warp_utils::reject::beacon_state_error)?; + + let validator_index = match validator_id { + ValidatorId::Index(index) => *index as usize, + ValidatorId::PublicKey(pubkey) => { + if let Some(index) = state + .get_validator_index(pubkey) + .map_err(warp_utils::reject::beacon_state_error)? + { + index + } else { + return Ok(None); + } + } + }; + + Ok(validator_statuses + .statuses + .get(validator_index) + .map(|vote| ValidatorInclusionData { + is_slashed: vote.is_slashed, + is_withdrawable_in_current_epoch: vote.is_withdrawable_in_current_epoch, + is_active_in_current_epoch: vote.is_active_in_current_epoch, + is_active_in_previous_epoch: vote.is_active_in_previous_epoch, + current_epoch_effective_balance_gwei: vote.current_epoch_effective_balance, + is_current_epoch_attester: vote.is_current_epoch_attester, + is_current_epoch_target_attester: vote.is_current_epoch_target_attester, + is_previous_epoch_attester: vote.is_previous_epoch_attester, + is_previous_epoch_target_attester: vote.is_previous_epoch_target_attester, + is_previous_epoch_head_attester: vote.is_previous_epoch_head_attester, + })) +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs new file mode 100644 index 000000000..2a7e8f6d4 --- /dev/null +++ b/beacon_node/http_api/tests/tests.rs @@ -0,0 +1,1786 @@ +use beacon_chain::{ + test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, + BlockingMigratorEphemeralHarnessType, + }, + BeaconChain, StateSkipConfig, +}; +use discv5::enr::{CombinedKey, EnrBuilder}; +use environment::null_logger; +use eth2::{types::*, BeaconNodeHttpClient, Url}; +use eth2_libp2p::{ + rpc::methods::MetaData, + types::{EnrBitfield, SyncState}, + NetworkGlobals, +}; +use http_api::{Config, Context}; +use network::NetworkMessage; +use state_processing::per_slot_processing; +use std::convert::TryInto; +use std::net::Ipv4Addr; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tree_hash::TreeHash; +use types::{ + test_utils::generate_deterministic_keypairs, AggregateSignature, BeaconState, BitList, Domain, + EthSpec, Hash256, Keypair, MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, Slot, +}; + +type E = MainnetEthSpec; + +const SLOTS_PER_EPOCH: u64 = 32; +const VALIDATOR_COUNT: usize = SLOTS_PER_EPOCH as usize; +const CHAIN_LENGTH: u64 = SLOTS_PER_EPOCH * 5; +const JUSTIFIED_EPOCH: u64 = 4; +const FINALIZED_EPOCH: u64 = 3; + +/// Skipping the slots around the epoch boundary allows us to check that we're obtaining states +/// from skipped slots for the finalized and justified checkpoints (instead of the state from the +/// block that those roots point to). +const SKIPPED_SLOTS: &[u64] = &[ + JUSTIFIED_EPOCH * SLOTS_PER_EPOCH - 1, + JUSTIFIED_EPOCH * SLOTS_PER_EPOCH, + FINALIZED_EPOCH * SLOTS_PER_EPOCH - 1, + FINALIZED_EPOCH * SLOTS_PER_EPOCH, +]; + +struct ApiTester { + chain: Arc<BeaconChain<BlockingMigratorEphemeralHarnessType<E>>>, + client: BeaconNodeHttpClient, + next_block: SignedBeaconBlock<E>, + attestations: Vec<Attestation<E>>, + attester_slashing: AttesterSlashing<E>, + proposer_slashing: ProposerSlashing, + voluntary_exit: SignedVoluntaryExit, + _server_shutdown: oneshot::Sender<()>, + validator_keypairs: Vec<Keypair>, + network_rx: mpsc::UnboundedReceiver<NetworkMessage<E>>, +} + +impl ApiTester { + pub fn new() -> Self { + let mut harness = BeaconChainHarness::new( + MainnetEthSpec, + generate_deterministic_keypairs(VALIDATOR_COUNT), + ); + + harness.advance_slot(); + + for _ in 0..CHAIN_LENGTH { + let slot = harness.chain.slot().unwrap().as_u64(); + + if !SKIPPED_SLOTS.contains(&slot) { + harness.extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + } + + harness.advance_slot(); + } + + let head = harness.chain.head().unwrap(); + + assert_eq!( + harness.chain.slot().unwrap(), + head.beacon_block.slot() + 1, + "precondition: current slot is one after head" + ); + + let (next_block, _next_state) = + harness.make_block(head.beacon_state.clone(), harness.chain.slot().unwrap()); + + let attestations = harness + .get_unaggregated_attestations( + &AttestationStrategy::AllValidators, + &head.beacon_state, + head.beacon_block_root, + harness.chain.slot().unwrap(), + ) + .into_iter() + .map(|vec| vec.into_iter().map(|(attestation, _subnet_id)| attestation)) + .flatten() + .collect::<Vec<_>>(); + + assert!( + !attestations.is_empty(), + "precondition: attestations for testing" + ); + + let attester_slashing = harness.make_attester_slashing(vec![0, 1]); + let proposer_slashing = harness.make_proposer_slashing(2); + let voluntary_exit = harness.make_voluntary_exit(3, harness.chain.epoch().unwrap()); + + // Changing this *after* the chain has been initialized is a bit cheeky, but it shouldn't + // cause issue. + // + // This allows for testing voluntary exits without building out a massive chain. + harness.chain.spec.shard_committee_period = 2; + + let chain = Arc::new(harness.chain); + + assert_eq!( + chain.head_info().unwrap().finalized_checkpoint.epoch, + 3, + "precondition: finality" + ); + assert_eq!( + chain + .head_info() + .unwrap() + .current_justified_checkpoint + .epoch, + 4, + "precondition: justification" + ); + + let (network_tx, network_rx) = mpsc::unbounded_channel(); + + let log = null_logger().unwrap(); + + // Default metadata + let meta_data = MetaData { + seq_number: 0, + attnets: EnrBitfield::<MinimalEthSpec>::default(), + }; + let enr_key = CombinedKey::generate_secp256k1(); + let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); + let network_globals = NetworkGlobals::new(enr, 42, 42, meta_data, vec![], &log); + + *network_globals.sync_state.write() = SyncState::Synced; + + let context = Arc::new(Context { + config: Config { + enabled: true, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 0, + allow_origin: None, + }, + chain: Some(chain.clone()), + network_tx: Some(network_tx), + network_globals: Some(Arc::new(network_globals)), + log, + }); + let ctx = context.clone(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_shutdown = async { + // It's not really interesting why this triggered, just that it happened. + let _ = shutdown_rx.await; + }; + let (listening_socket, server) = http_api::serve(ctx, server_shutdown).unwrap(); + + tokio::spawn(async { server.await }); + + let client = BeaconNodeHttpClient::new( + Url::parse(&format!( + "http://{}:{}", + listening_socket.ip(), + listening_socket.port() + )) + .unwrap(), + ); + + Self { + chain, + client, + next_block, + attestations, + attester_slashing, + proposer_slashing, + voluntary_exit, + _server_shutdown: shutdown_tx, + validator_keypairs: harness.validators_keypairs, + network_rx, + } + } + + fn skip_slots(self, count: u64) -> Self { + for _ in 0..count { + self.chain + .slot_clock + .set_slot(self.chain.slot().unwrap().as_u64() + 1); + } + + self + } + + fn interesting_state_ids(&self) -> Vec<StateId> { + let mut ids = vec![ + StateId::Head, + StateId::Genesis, + StateId::Finalized, + StateId::Justified, + StateId::Slot(Slot::new(0)), + StateId::Slot(Slot::new(32)), + StateId::Slot(Slot::from(SKIPPED_SLOTS[0])), + StateId::Slot(Slot::from(SKIPPED_SLOTS[1])), + StateId::Slot(Slot::from(SKIPPED_SLOTS[2])), + StateId::Slot(Slot::from(SKIPPED_SLOTS[3])), + StateId::Root(Hash256::zero()), + ]; + ids.push(StateId::Root(self.chain.head_info().unwrap().state_root)); + ids + } + + fn interesting_block_ids(&self) -> Vec<BlockId> { + let mut ids = vec![ + BlockId::Head, + BlockId::Genesis, + BlockId::Finalized, + BlockId::Justified, + BlockId::Slot(Slot::new(0)), + BlockId::Slot(Slot::new(32)), + BlockId::Slot(Slot::from(SKIPPED_SLOTS[0])), + BlockId::Slot(Slot::from(SKIPPED_SLOTS[1])), + BlockId::Slot(Slot::from(SKIPPED_SLOTS[2])), + BlockId::Slot(Slot::from(SKIPPED_SLOTS[3])), + BlockId::Root(Hash256::zero()), + ]; + ids.push(BlockId::Root(self.chain.head_info().unwrap().block_root)); + ids + } + + fn get_state(&self, state_id: StateId) -> Option<BeaconState<E>> { + match state_id { + StateId::Head => Some(self.chain.head().unwrap().beacon_state), + StateId::Genesis => self + .chain + .get_state(&self.chain.genesis_state_root, None) + .unwrap(), + StateId::Finalized => { + let finalized_slot = self + .chain + .head_info() + .unwrap() + .finalized_checkpoint + .epoch + .start_slot(E::slots_per_epoch()); + + let root = self + .chain + .state_root_at_slot(finalized_slot) + .unwrap() + .unwrap(); + + self.chain.get_state(&root, Some(finalized_slot)).unwrap() + } + StateId::Justified => { + let justified_slot = self + .chain + .head_info() + .unwrap() + .current_justified_checkpoint + .epoch + .start_slot(E::slots_per_epoch()); + + let root = self + .chain + .state_root_at_slot(justified_slot) + .unwrap() + .unwrap(); + + self.chain.get_state(&root, Some(justified_slot)).unwrap() + } + StateId::Slot(slot) => { + let root = self.chain.state_root_at_slot(slot).unwrap().unwrap(); + + self.chain.get_state(&root, Some(slot)).unwrap() + } + StateId::Root(root) => self.chain.get_state(&root, None).unwrap(), + } + } + + pub async fn test_beacon_genesis(self) -> Self { + let result = self.client.get_beacon_genesis().await.unwrap().data; + + let state = self.chain.head().unwrap().beacon_state; + let expected = GenesisData { + genesis_time: state.genesis_time, + genesis_validators_root: state.genesis_validators_root, + genesis_fork_version: self.chain.spec.genesis_fork_version, + }; + + assert_eq!(result, expected); + + self + } + + pub async fn test_beacon_states_root(self) -> Self { + for state_id in self.interesting_state_ids() { + let result = self + .client + .get_beacon_states_root(state_id) + .await + .unwrap() + .map(|res| res.data.root); + + let expected = match state_id { + StateId::Head => Some(self.chain.head_info().unwrap().state_root), + StateId::Genesis => Some(self.chain.genesis_state_root), + StateId::Finalized => { + let finalized_slot = self + .chain + .head_info() + .unwrap() + .finalized_checkpoint + .epoch + .start_slot(E::slots_per_epoch()); + + self.chain.state_root_at_slot(finalized_slot).unwrap() + } + StateId::Justified => { + let justified_slot = self + .chain + .head_info() + .unwrap() + .current_justified_checkpoint + .epoch + .start_slot(E::slots_per_epoch()); + + self.chain.state_root_at_slot(justified_slot).unwrap() + } + StateId::Slot(slot) => self.chain.state_root_at_slot(slot).unwrap(), + StateId::Root(root) => Some(root), + }; + + assert_eq!(result, expected, "{:?}", state_id); + } + + self + } + + pub async fn test_beacon_states_fork(self) -> Self { + for state_id in self.interesting_state_ids() { + let result = self + .client + .get_beacon_states_fork(state_id) + .await + .unwrap() + .map(|res| res.data); + + let expected = self.get_state(state_id).map(|state| state.fork); + + assert_eq!(result, expected, "{:?}", state_id); + } + + self + } + + pub async fn test_beacon_states_finality_checkpoints(self) -> Self { + for state_id in self.interesting_state_ids() { + let result = self + .client + .get_beacon_states_finality_checkpoints(state_id) + .await + .unwrap() + .map(|res| res.data); + + let expected = self + .get_state(state_id) + .map(|state| FinalityCheckpointsData { + previous_justified: state.previous_justified_checkpoint, + current_justified: state.current_justified_checkpoint, + finalized: state.finalized_checkpoint, + }); + + assert_eq!(result, expected, "{:?}", state_id); + } + + self + } + + pub async fn test_beacon_states_validators(self) -> Self { + for state_id in self.interesting_state_ids() { + let result = self + .client + .get_beacon_states_validators(state_id) + .await + .unwrap() + .map(|res| res.data); + + let expected = self.get_state(state_id).map(|state| { + let epoch = state.current_epoch(); + let finalized_epoch = state.finalized_checkpoint.epoch; + let far_future_epoch = self.chain.spec.far_future_epoch; + + let mut validators = Vec::with_capacity(state.validators.len()); + + for i in 0..state.validators.len() { + let validator = state.validators[i].clone(); + + validators.push(ValidatorData { + index: i as u64, + balance: state.balances[i], + status: ValidatorStatus::from_validator( + Some(&validator), + epoch, + finalized_epoch, + far_future_epoch, + ), + validator, + }) + } + + validators + }); + + assert_eq!(result, expected, "{:?}", state_id); + } + + self + } + + pub async fn test_beacon_states_validator_id(self) -> Self { + for state_id in self.interesting_state_ids() { + let state_opt = self.get_state(state_id); + let validators = match state_opt.as_ref() { + Some(state) => state.validators.clone().into(), + None => vec![], + }; + + for (i, validator) in validators.into_iter().enumerate() { + let validator_ids = &[ + ValidatorId::PublicKey(validator.pubkey.clone()), + ValidatorId::Index(i as u64), + ]; + + for validator_id in validator_ids { + let result = self + .client + .get_beacon_states_validator_id(state_id, validator_id) + .await + .unwrap() + .map(|res| res.data); + + if result.is_none() && state_opt.is_none() { + continue; + } + + let state = state_opt.as_ref().expect("result should be none"); + + let expected = { + let epoch = state.current_epoch(); + let finalized_epoch = state.finalized_checkpoint.epoch; + let far_future_epoch = self.chain.spec.far_future_epoch; + + ValidatorData { + index: i as u64, + balance: state.balances[i], + status: ValidatorStatus::from_validator( + Some(&validator), + epoch, + finalized_epoch, + far_future_epoch, + ), + validator: validator.clone(), + } + }; + + assert_eq!(result, Some(expected), "{:?}, {:?}", state_id, validator_id); + } + } + } + + self + } + + pub async fn test_beacon_states_committees(self) -> Self { + for state_id in self.interesting_state_ids() { + let mut state_opt = self.get_state(state_id); + + let epoch = state_opt + .as_ref() + .map(|state| state.current_epoch()) + .unwrap_or_else(|| Epoch::new(0)); + + let results = self + .client + .get_beacon_states_committees(state_id, epoch, None, None) + .await + .unwrap() + .map(|res| res.data); + + if results.is_none() && state_opt.is_none() { + continue; + } + + let state = state_opt.as_mut().expect("result should be none"); + state.build_all_committee_caches(&self.chain.spec).unwrap(); + let committees = state + .get_beacon_committees_at_epoch( + RelativeEpoch::from_epoch(state.current_epoch(), epoch).unwrap(), + ) + .unwrap(); + + for (i, result) in results.unwrap().into_iter().enumerate() { + let expected = &committees[i]; + + assert_eq!(result.index, expected.index, "{}", state_id); + assert_eq!(result.slot, expected.slot, "{}", state_id); + assert_eq!( + result + .validators + .into_iter() + .map(|i| i as usize) + .collect::<Vec<_>>(), + expected.committee.to_vec(), + "{}", + state_id + ); + } + } + + self + } + + fn get_block_root(&self, block_id: BlockId) -> Option<Hash256> { + match block_id { + BlockId::Head => Some(self.chain.head_info().unwrap().block_root), + BlockId::Genesis => Some(self.chain.genesis_block_root), + BlockId::Finalized => Some(self.chain.head_info().unwrap().finalized_checkpoint.root), + BlockId::Justified => Some( + self.chain + .head_info() + .unwrap() + .current_justified_checkpoint + .root, + ), + BlockId::Slot(slot) => self.chain.block_root_at_slot(slot).unwrap(), + BlockId::Root(root) => Some(root), + } + } + + fn get_block(&self, block_id: BlockId) -> Option<SignedBeaconBlock<E>> { + let root = self.get_block_root(block_id); + root.and_then(|root| self.chain.get_block(&root).unwrap()) + } + + pub async fn test_beacon_headers_all_slots(self) -> Self { + for slot in 0..CHAIN_LENGTH { + let slot = Slot::from(slot); + + let result = self + .client + .get_beacon_headers(Some(slot), None) + .await + .unwrap() + .map(|res| res.data); + + let root = self.chain.block_root_at_slot(slot).unwrap(); + + if root.is_none() && result.is_none() { + continue; + } + + let root = root.unwrap(); + let block = self.chain.block_at_slot(slot).unwrap().unwrap(); + let header = BlockHeaderData { + root, + canonical: true, + header: BlockHeaderAndSignature { + message: block.message.block_header(), + signature: block.signature.into(), + }, + }; + let expected = vec![header]; + + assert_eq!(result.unwrap(), expected, "slot {:?}", slot); + } + + self + } + + pub async fn test_beacon_headers_all_parents(self) -> Self { + let mut roots = self + .chain + .rev_iter_block_roots() + .unwrap() + .map(Result::unwrap) + .map(|(root, _slot)| root) + .collect::<Vec<_>>() + .into_iter() + .rev() + .collect::<Vec<_>>(); + + // The iterator natively returns duplicate roots for skipped slots. + roots.dedup(); + + for i in 1..roots.len() { + let parent_root = roots[i - 1]; + let child_root = roots[i]; + + let result = self + .client + .get_beacon_headers(None, Some(parent_root)) + .await + .unwrap() + .unwrap() + .data; + + assert_eq!(result.len(), 1, "i {}", i); + assert_eq!(result[0].root, child_root, "i {}", i); + } + + self + } + + pub async fn test_beacon_headers_block_id(self) -> Self { + for block_id in self.interesting_block_ids() { + let result = self + .client + .get_beacon_headers_block_id(block_id) + .await + .unwrap() + .map(|res| res.data); + + let block_root_opt = self.get_block_root(block_id); + + let block_opt = block_root_opt.and_then(|root| self.chain.get_block(&root).unwrap()); + + if block_opt.is_none() && result.is_none() { + continue; + } + + let result = result.unwrap(); + let block = block_opt.unwrap(); + let block_root = block_root_opt.unwrap(); + let canonical = self + .chain + .block_root_at_slot(block.slot()) + .unwrap() + .map_or(false, |canonical| block_root == canonical); + + assert_eq!(result.canonical, canonical, "{:?}", block_id); + assert_eq!(result.root, block_root, "{:?}", block_id); + assert_eq!( + result.header.message, + block.message.block_header(), + "{:?}", + block_id + ); + assert_eq!( + result.header.signature, + block.signature.into(), + "{:?}", + block_id + ); + } + + self + } + + pub async fn test_beacon_blocks_root(self) -> Self { + for block_id in self.interesting_block_ids() { + let result = self + .client + .get_beacon_blocks_root(block_id) + .await + .unwrap() + .map(|res| res.data.root); + + let expected = self.get_block_root(block_id); + + assert_eq!(result, expected, "{:?}", block_id); + } + + self + } + + pub async fn test_post_beacon_blocks_valid(mut self) -> Self { + let next_block = &self.next_block; + + self.client.post_beacon_blocks(next_block).await.unwrap(); + + assert!( + self.network_rx.try_recv().is_ok(), + "valid blocks should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_blocks_invalid(mut self) -> Self { + let mut next_block = self.next_block.clone(); + next_block.message.proposer_index += 1; + + assert!(self.client.post_beacon_blocks(&next_block).await.is_err()); + + assert!( + self.network_rx.try_recv().is_ok(), + "invalid blocks should be sent to network" + ); + + self + } + + pub async fn test_beacon_blocks(self) -> Self { + for block_id in self.interesting_block_ids() { + let result = self + .client + .get_beacon_blocks(block_id) + .await + .unwrap() + .map(|res| res.data); + + let expected = self.get_block(block_id); + + assert_eq!(result, expected, "{:?}", block_id); + } + + self + } + + pub async fn test_beacon_blocks_attestations(self) -> Self { + for block_id in self.interesting_block_ids() { + let result = self + .client + .get_beacon_blocks_attestations(block_id) + .await + .unwrap() + .map(|res| res.data); + + let expected = self + .get_block(block_id) + .map(|block| block.message.body.attestations.into()); + + assert_eq!(result, expected, "{:?}", block_id); + } + + self + } + + pub async fn test_post_beacon_pool_attestations_valid(mut self) -> Self { + for attestation in &self.attestations { + self.client + .post_beacon_pool_attestations(attestation) + .await + .unwrap(); + + assert!( + self.network_rx.try_recv().is_ok(), + "valid attestation should be sent to network" + ); + } + + self + } + + pub async fn test_post_beacon_pool_attestations_invalid(mut self) -> Self { + for attestation in &self.attestations { + let mut attestation = attestation.clone(); + attestation.data.slot += 1; + + assert!(self + .client + .post_beacon_pool_attestations(&attestation) + .await + .is_err()); + + assert!( + self.network_rx.try_recv().is_err(), + "invalid attestation should not be sent to network" + ); + } + + self + } + + pub async fn test_get_beacon_pool_attestations(self) -> Self { + let result = self + .client + .get_beacon_pool_attestations() + .await + .unwrap() + .data; + + let mut expected = self.chain.op_pool.get_all_attestations(); + expected.extend(self.chain.naive_aggregation_pool.read().iter().cloned()); + + assert_eq!(result, expected); + + self + } + + pub async fn test_post_beacon_pool_attester_slashings_valid(mut self) -> Self { + self.client + .post_beacon_pool_attester_slashings(&self.attester_slashing) + .await + .unwrap(); + + assert!( + self.network_rx.try_recv().is_ok(), + "valid attester slashing should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_pool_attester_slashings_invalid(mut self) -> Self { + let mut slashing = self.attester_slashing.clone(); + slashing.attestation_1.data.slot += 1; + + self.client + .post_beacon_pool_attester_slashings(&slashing) + .await + .unwrap_err(); + + assert!( + self.network_rx.try_recv().is_err(), + "invalid attester slashing should not be sent to network" + ); + + self + } + + pub async fn test_get_beacon_pool_attester_slashings(self) -> Self { + let result = self + .client + .get_beacon_pool_attester_slashings() + .await + .unwrap() + .data; + + let expected = self.chain.op_pool.get_all_attester_slashings(); + + assert_eq!(result, expected); + + self + } + + pub async fn test_post_beacon_pool_proposer_slashings_valid(mut self) -> Self { + self.client + .post_beacon_pool_proposer_slashings(&self.proposer_slashing) + .await + .unwrap(); + + assert!( + self.network_rx.try_recv().is_ok(), + "valid proposer slashing should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_pool_proposer_slashings_invalid(mut self) -> Self { + let mut slashing = self.proposer_slashing.clone(); + slashing.signed_header_1.message.slot += 1; + + self.client + .post_beacon_pool_proposer_slashings(&slashing) + .await + .unwrap_err(); + + assert!( + self.network_rx.try_recv().is_err(), + "invalid proposer slashing should not be sent to network" + ); + + self + } + + pub async fn test_get_beacon_pool_proposer_slashings(self) -> Self { + let result = self + .client + .get_beacon_pool_proposer_slashings() + .await + .unwrap() + .data; + + let expected = self.chain.op_pool.get_all_proposer_slashings(); + + assert_eq!(result, expected); + + self + } + + pub async fn test_post_beacon_pool_voluntary_exits_valid(mut self) -> Self { + self.client + .post_beacon_pool_voluntary_exits(&self.voluntary_exit) + .await + .unwrap(); + + assert!( + self.network_rx.try_recv().is_ok(), + "valid exit should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_pool_voluntary_exits_invalid(mut self) -> Self { + let mut exit = self.voluntary_exit.clone(); + exit.message.epoch += 1; + + self.client + .post_beacon_pool_voluntary_exits(&exit) + .await + .unwrap_err(); + + assert!( + self.network_rx.try_recv().is_err(), + "invalid exit should not be sent to network" + ); + + self + } + + pub async fn test_get_beacon_pool_voluntary_exits(self) -> Self { + let result = self + .client + .get_beacon_pool_voluntary_exits() + .await + .unwrap() + .data; + + let expected = self.chain.op_pool.get_all_voluntary_exits(); + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_config_fork_schedule(self) -> Self { + let result = self.client.get_config_fork_schedule().await.unwrap().data; + + let expected = vec![self.chain.head_info().unwrap().fork]; + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_config_spec(self) -> Self { + let result = self.client.get_config_spec().await.unwrap().data; + + let expected = YamlConfig::from_spec::<E>(&self.chain.spec); + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_config_deposit_contract(self) -> Self { + let result = self + .client + .get_config_deposit_contract() + .await + .unwrap() + .data; + + let expected = DepositContractData { + address: self.chain.spec.deposit_contract_address, + chain_id: eth1::DEFAULT_NETWORK_ID.into(), + }; + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_node_version(self) -> Self { + let result = self.client.get_node_version().await.unwrap().data; + + let expected = VersionData { + version: lighthouse_version::version_with_platform(), + }; + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_node_syncing(self) -> Self { + let result = self.client.get_node_syncing().await.unwrap().data; + let head_slot = self.chain.head_info().unwrap().slot; + let sync_distance = self.chain.slot().unwrap() - head_slot; + + let expected = SyncingData { + is_syncing: false, + head_slot, + sync_distance, + }; + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_debug_beacon_states(self) -> Self { + for state_id in self.interesting_state_ids() { + let result = self + .client + .get_debug_beacon_states(state_id) + .await + .unwrap() + .map(|res| res.data); + + let mut expected = self.get_state(state_id); + expected.as_mut().map(|state| state.drop_all_caches()); + + assert_eq!(result, expected, "{:?}", state_id); + } + + self + } + + pub async fn test_get_debug_beacon_heads(self) -> Self { + let result = self + .client + .get_debug_beacon_heads() + .await + .unwrap() + .data + .into_iter() + .map(|head| (head.root, head.slot)) + .collect::<Vec<_>>(); + + let expected = self.chain.heads(); + + assert_eq!(result, expected); + + self + } + + fn validator_count(&self) -> usize { + self.chain.head().unwrap().beacon_state.validators.len() + } + + fn interesting_validator_indices(&self) -> Vec<Vec<u64>> { + let validator_count = self.validator_count() as u64; + + let mut interesting = vec![ + vec![], + vec![0], + vec![0, 1], + vec![0, 1, 3], + vec![validator_count], + vec![validator_count, 1], + vec![validator_count, 1, 3], + vec![u64::max_value()], + vec![u64::max_value(), 1], + vec![u64::max_value(), 1, 3], + ]; + + interesting.push((0..validator_count).collect()); + + interesting + } + + pub async fn test_get_validator_duties_attester(self) -> Self { + let current_epoch = self.chain.epoch().unwrap().as_u64(); + + let half = current_epoch / 2; + let first = current_epoch - half; + let last = current_epoch + half; + + for epoch in first..=last { + for indices in self.interesting_validator_indices() { + let epoch = Epoch::from(epoch); + + // The endpoint does not allow getting duties past the next epoch. + if epoch > current_epoch + 1 { + assert_eq!( + self.client + .get_validator_duties_attester(epoch, Some(&indices)) + .await + .unwrap_err() + .status() + .map(Into::into), + Some(400) + ); + continue; + } + + let results = self + .client + .get_validator_duties_attester(epoch, Some(&indices)) + .await + .unwrap() + .data; + + let mut state = self + .chain + .state_at_slot( + epoch.start_slot(E::slots_per_epoch()), + StateSkipConfig::WithStateRoots, + ) + .unwrap(); + state + .build_committee_cache(RelativeEpoch::Current, &self.chain.spec) + .unwrap(); + + let expected_len = indices + .iter() + .filter(|i| **i < state.validators.len() as u64) + .count(); + + assert_eq!(results.len(), expected_len); + + for (indices_set, &i) in indices.iter().enumerate() { + if let Some(duty) = state + .get_attestation_duties(i as usize, RelativeEpoch::Current) + .unwrap() + { + let expected = AttesterData { + pubkey: state.validators[i as usize].pubkey.clone().into(), + validator_index: i, + committees_at_slot: duty.committees_at_slot, + committee_index: duty.index, + committee_length: duty.committee_len as u64, + validator_committee_index: duty.committee_position as u64, + slot: duty.slot, + }; + + let result = results + .iter() + .find(|duty| duty.validator_index == i) + .unwrap(); + + assert_eq!( + *result, expected, + "epoch: {}, indices_set: {}", + epoch, indices_set + ); + } else { + assert!( + !results.iter().any(|duty| duty.validator_index == i), + "validator index should not exist in response" + ); + } + } + } + } + + self + } + + pub async fn test_get_validator_duties_proposer(self) -> Self { + let current_epoch = self.chain.epoch().unwrap(); + + let result = self + .client + .get_validator_duties_proposer(current_epoch) + .await + .unwrap() + .data; + + let mut state = self.chain.head_beacon_state().unwrap(); + + while state.current_epoch() < current_epoch { + per_slot_processing(&mut state, None, &self.chain.spec).unwrap(); + } + + state + .build_committee_cache(RelativeEpoch::Current, &self.chain.spec) + .unwrap(); + + let expected = current_epoch + .slot_iter(E::slots_per_epoch()) + .map(|slot| { + let index = state + .get_beacon_proposer_index(slot, &self.chain.spec) + .unwrap(); + let pubkey = state.validators[index].pubkey.clone().into(); + + ProposerData { pubkey, slot } + }) + .collect::<Vec<_>>(); + + assert_eq!(result, expected); + + self + } + + pub async fn test_block_production(self) -> Self { + let fork = self.chain.head_info().unwrap().fork; + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = self.chain.spec.get_domain( + epoch, + Domain::Randao, + &fork, + genesis_validators_root, + ); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + let block = self + .client + .get_validator_blocks::<E>(slot, randao_reveal, None) + .await + .unwrap() + .data; + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + + self.client.post_beacon_blocks(&signed_block).await.unwrap(); + + assert_eq!(self.chain.head_beacon_block().unwrap(), signed_block); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + + pub async fn test_get_validator_attestation_data(self) -> Self { + let mut state = self.chain.head_beacon_state().unwrap(); + let slot = state.slot; + state + .build_committee_cache(RelativeEpoch::Current, &self.chain.spec) + .unwrap(); + + for index in 0..state.get_committee_count_at_slot(slot).unwrap() { + let result = self + .client + .get_validator_attestation_data(slot, index) + .await + .unwrap() + .data; + + let expected = self + .chain + .produce_unaggregated_attestation(slot, index) + .unwrap() + .data; + + assert_eq!(result, expected); + } + + self + } + + pub async fn test_get_validator_aggregate_attestation(self) -> Self { + let attestation = self + .chain + .head_beacon_block() + .unwrap() + .message + .body + .attestations[0] + .clone(); + + let result = self + .client + .get_validator_aggregate_attestation( + attestation.data.slot, + attestation.data.tree_hash_root(), + ) + .await + .unwrap() + .unwrap() + .data; + + let expected = attestation; + + assert_eq!(result, expected); + + self + } + + pub async fn get_aggregate(&mut self) -> SignedAggregateAndProof<E> { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let mut head = self.chain.head().unwrap(); + while head.beacon_state.current_epoch() < epoch { + per_slot_processing(&mut head.beacon_state, None, &self.chain.spec).unwrap(); + } + head.beacon_state + .build_committee_cache(RelativeEpoch::Current, &self.chain.spec) + .unwrap(); + + let committee_len = head.beacon_state.get_committee_count_at_slot(slot).unwrap(); + let fork = head.beacon_state.fork; + let genesis_validators_root = self.chain.genesis_validators_root; + + let mut duties = vec![]; + for i in 0..self.validator_keypairs.len() { + duties.push( + self.client + .get_validator_duties_attester(epoch, Some(&[i as u64])) + .await + .unwrap() + .data[0] + .clone(), + ) + } + + let (i, kp, duty, proof) = self + .validator_keypairs + .iter() + .enumerate() + .find_map(|(i, kp)| { + let duty = duties[i].clone(); + + let proof = SelectionProof::new::<E>( + duty.slot, + &kp.sk, + &fork, + genesis_validators_root, + &self.chain.spec, + ); + + if proof + .is_aggregator(committee_len as usize, &self.chain.spec) + .unwrap() + { + Some((i, kp, duty, proof)) + } else { + None + } + }) + .expect("there is at least one aggregator for this epoch") + .clone(); + + if duty.slot > slot { + self.chain.slot_clock.set_slot(duty.slot.into()); + } + + let attestation_data = self + .client + .get_validator_attestation_data(duty.slot, duty.committee_index) + .await + .unwrap() + .data; + + let mut attestation = Attestation { + aggregation_bits: BitList::with_capacity(duty.committee_length as usize).unwrap(), + data: attestation_data, + signature: AggregateSignature::infinity(), + }; + + attestation + .sign( + &kp.sk, + duty.validator_committee_index as usize, + &fork, + genesis_validators_root, + &self.chain.spec, + ) + .unwrap(); + + SignedAggregateAndProof::from_aggregate( + i as u64, + attestation, + Some(proof), + &kp.sk, + &fork, + genesis_validators_root, + &self.chain.spec, + ) + } + + pub async fn test_get_validator_aggregate_and_proofs_valid(mut self) -> Self { + let aggregate = self.get_aggregate().await; + + self.client + .post_validator_aggregate_and_proof::<E>(&aggregate) + .await + .unwrap(); + + assert!(self.network_rx.try_recv().is_ok()); + + self + } + + pub async fn test_get_validator_aggregate_and_proofs_invalid(mut self) -> Self { + let mut aggregate = self.get_aggregate().await; + + aggregate.message.aggregate.data.slot += 1; + + self.client + .post_validator_aggregate_and_proof::<E>(&aggregate) + .await + .unwrap_err(); + + assert!(self.network_rx.try_recv().is_err()); + + self + } + + pub async fn test_get_validator_beacon_committee_subscriptions(mut self) -> Self { + let subscription = BeaconCommitteeSubscription { + validator_index: 0, + committee_index: 0, + committees_at_slot: 1, + slot: Slot::new(1), + is_aggregator: true, + }; + + self.client + .post_validator_beacon_committee_subscriptions(&[subscription]) + .await + .unwrap(); + + self.network_rx.try_recv().unwrap(); + + self + } + + #[cfg(target_os = "linux")] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap(); + + self + } + + #[cfg(not(target_os = "linux"))] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap_err(); + + self + } + + pub async fn test_get_lighthouse_syncing(self) -> Self { + self.client.get_lighthouse_syncing().await.unwrap(); + + self + } + + pub async fn test_get_lighthouse_proto_array(self) -> Self { + self.client.get_lighthouse_proto_array().await.unwrap(); + + self + } + + pub async fn test_get_lighthouse_validator_inclusion_global(self) -> Self { + let epoch = self.chain.epoch().unwrap() - 1; + self.client + .get_lighthouse_validator_inclusion_global(epoch) + .await + .unwrap(); + + self + } + + pub async fn test_get_lighthouse_validator_inclusion(self) -> Self { + let epoch = self.chain.epoch().unwrap() - 1; + self.client + .get_lighthouse_validator_inclusion(epoch, ValidatorId::Index(0)) + .await + .unwrap(); + + self + } +} + +#[tokio::test(core_threads = 2)] +async fn beacon_genesis() { + ApiTester::new().test_beacon_genesis().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_states_root() { + ApiTester::new().test_beacon_states_root().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_states_fork() { + ApiTester::new().test_beacon_states_fork().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_states_finality_checkpoints() { + ApiTester::new() + .test_beacon_states_finality_checkpoints() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_states_validators() { + ApiTester::new().test_beacon_states_validators().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_states_committees() { + ApiTester::new().test_beacon_states_committees().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_states_validator_id() { + ApiTester::new().test_beacon_states_validator_id().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_headers() { + ApiTester::new() + .test_beacon_headers_all_slots() + .await + .test_beacon_headers_all_parents() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_headers_block_id() { + ApiTester::new().test_beacon_headers_block_id().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_blocks() { + ApiTester::new().test_beacon_blocks().await; +} + +#[tokio::test(core_threads = 2)] +async fn post_beacon_blocks_valid() { + ApiTester::new().test_post_beacon_blocks_valid().await; +} + +#[tokio::test(core_threads = 2)] +async fn post_beacon_blocks_invalid() { + ApiTester::new().test_post_beacon_blocks_invalid().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_blocks_root() { + ApiTester::new().test_beacon_blocks_root().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_blocks_attestations() { + ApiTester::new().test_beacon_blocks_attestations().await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_get() { + ApiTester::new() + .test_get_beacon_pool_attestations() + .await + .test_get_beacon_pool_attester_slashings() + .await + .test_get_beacon_pool_proposer_slashings() + .await + .test_get_beacon_pool_voluntary_exits() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_attestations_valid() { + ApiTester::new() + .test_post_beacon_pool_attestations_valid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_attestations_invalid() { + ApiTester::new() + .test_post_beacon_pool_attestations_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_attester_slashings_valid() { + ApiTester::new() + .test_post_beacon_pool_attester_slashings_valid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_attester_slashings_invalid() { + ApiTester::new() + .test_post_beacon_pool_attester_slashings_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_proposer_slashings_valid() { + ApiTester::new() + .test_post_beacon_pool_proposer_slashings_valid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_proposer_slashings_invalid() { + ApiTester::new() + .test_post_beacon_pool_proposer_slashings_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_voluntary_exits_valid() { + ApiTester::new() + .test_post_beacon_pool_voluntary_exits_valid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn beacon_pools_post_voluntary_exits_invalid() { + ApiTester::new() + .test_post_beacon_pool_voluntary_exits_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn config_get() { + ApiTester::new() + .test_get_config_fork_schedule() + .await + .test_get_config_spec() + .await + .test_get_config_deposit_contract() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn debug_get() { + ApiTester::new() + .test_get_debug_beacon_states() + .await + .test_get_debug_beacon_heads() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn node_get() { + ApiTester::new() + .test_get_node_version() + .await + .test_get_node_syncing() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_duties_attester() { + ApiTester::new().test_get_validator_duties_attester().await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_duties_attester_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_duties_attester() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_duties_proposer() { + ApiTester::new().test_get_validator_duties_proposer().await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_duties_proposer_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_duties_proposer() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn block_production() { + ApiTester::new().test_block_production().await; +} + +#[tokio::test(core_threads = 2)] +async fn block_production_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_block_production() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_attestation_data() { + ApiTester::new().test_get_validator_attestation_data().await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_attestation_data_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_attestation_data() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_aggregate_attestation() { + ApiTester::new() + .test_get_validator_aggregate_attestation() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_aggregate_attestation_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_aggregate_attestation() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_aggregate_and_proofs_valid() { + ApiTester::new() + .test_get_validator_aggregate_and_proofs_valid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_aggregate_and_proofs_valid_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_aggregate_and_proofs_valid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_aggregate_and_proofs_invalid() { + ApiTester::new() + .test_get_validator_aggregate_and_proofs_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_aggregate_and_proofs_invalid_with_skip_slots() { + ApiTester::new() + .skip_slots(E::slots_per_epoch() * 2) + .test_get_validator_aggregate_and_proofs_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn get_validator_beacon_committee_subscriptions() { + ApiTester::new() + .test_get_validator_beacon_committee_subscriptions() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn lighthouse_endpoints() { + ApiTester::new() + .test_get_lighthouse_health() + .await + .test_get_lighthouse_syncing() + .await + .test_get_lighthouse_proto_array() + .await + .test_get_lighthouse_validator_inclusion() + .await + .test_get_lighthouse_validator_inclusion_global() + .await; +} diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml new file mode 100644 index 000000000..482f7a5de --- /dev/null +++ b/beacon_node/http_metrics/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "http_metrics" +version = "0.1.0" +authors = ["Paul Hauner <paul@paulhauner.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +prometheus = "0.9.0" +warp = "0.2.5" +serde = { version = "1.0.110", features = ["derive"] } +slog = "2.5.2" +beacon_chain = { path = "../beacon_chain" } +store = { path = "../store" } +eth2_libp2p = { path = "../eth2_libp2p" } +slot_clock = { path = "../../common/slot_clock" } +lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +lazy_static = "1.4.0" +eth2 = { path = "../../common/eth2" } +lighthouse_version = { path = "../../common/lighthouse_version" } +warp_utils = { path = "../../common/warp_utils" } + +[dev-dependencies] +tokio = { version = "0.2.21", features = ["sync"] } +reqwest = { version = "0.10.8", features = ["json"] } +environment = { path = "../../lighthouse/environment" } +types = { path = "../../consensus/types" } diff --git a/beacon_node/http_metrics/src/lib.rs b/beacon_node/http_metrics/src/lib.rs new file mode 100644 index 000000000..37eac82bd --- /dev/null +++ b/beacon_node/http_metrics/src/lib.rs @@ -0,0 +1,135 @@ +//! This crate provides a HTTP server that is solely dedicated to serving the `/metrics` endpoint. +//! +//! For other endpoints, see the `http_api` crate. + +#[macro_use] +extern crate lazy_static; + +mod metrics; + +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use lighthouse_version::version_with_platform; +use serde::{Deserialize, Serialize}; +use slog::{crit, info, Logger}; +use std::future::Future; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::path::PathBuf; +use std::sync::Arc; +use warp::{http::Response, Filter}; + +#[derive(Debug)] +pub enum Error { + Warp(warp::Error), + Other(String), +} + +impl From<warp::Error> for Error { + fn from(e: warp::Error) -> Self { + Error::Warp(e) + } +} + +impl From<String> for Error { + fn from(e: String) -> Self { + Error::Other(e) + } +} + +/// A wrapper around all the items required to spawn the HTTP server. +/// +/// The server will gracefully handle the case where any fields are `None`. +pub struct Context<T: BeaconChainTypes> { + pub config: Config, + pub chain: Option<Arc<BeaconChain<T>>>, + pub db_path: Option<PathBuf>, + pub freezer_db_path: Option<PathBuf>, + pub log: Logger, +} + +/// Configuration for the HTTP server. +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub enabled: bool, + pub listen_addr: Ipv4Addr, + pub listen_port: u16, + pub allow_origin: Option<String>, +} + +impl Default for Config { + fn default() -> Self { + Self { + enabled: false, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 5054, + allow_origin: None, + } + } +} + +/// Creates a server that will serve requests using information from `ctx`. +/// +/// The server will shut down gracefully when the `shutdown` future resolves. +/// +/// ## Returns +/// +/// This function will bind the server to the provided address and then return a tuple of: +/// +/// - `SocketAddr`: the address that the HTTP server will listen on. +/// - `Future`: the actual server future that will need to be awaited. +/// +/// ## Errors +/// +/// Returns an error if the server is unable to bind or there is another error during +/// configuration. +pub fn serve<T: BeaconChainTypes>( + ctx: Arc<Context<T>>, + shutdown: impl Future<Output = ()> + Send + Sync + 'static, +) -> Result<(SocketAddr, impl Future<Output = ()>), Error> { + let config = &ctx.config; + let log = ctx.log.clone(); + let allow_origin = config.allow_origin.clone(); + + // Sanity check. + if !config.enabled { + crit!(log, "Cannot start disabled metrics HTTP server"); + return Err(Error::Other( + "A disabled metrics server should not be started".to_string(), + )); + } + + let inner_ctx = ctx.clone(); + let routes = warp::get() + .and(warp::path("metrics")) + .map(move || inner_ctx.clone()) + .and_then(|ctx: Arc<Context<T>>| async move { + Ok::<_, warp::Rejection>( + metrics::gather_prometheus_metrics(&ctx) + .map(|body| Response::builder().status(200).body(body).unwrap()) + .unwrap_or_else(|e| { + Response::builder() + .status(500) + .body(format!("Unable to gather metrics: {:?}", e)) + .unwrap() + }), + ) + }) + // Add a `Server` header. + .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) + // Maybe add some CORS headers. + .map(move |reply| warp_utils::reply::maybe_cors(reply, allow_origin.as_ref())); + + let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( + SocketAddrV4::new(config.listen_addr, config.listen_port), + async { + shutdown.await; + }, + )?; + + info!( + log, + "Metrics HTTP server started"; + "listen_address" => listening_socket.to_string(), + ); + + Ok((listening_socket, server)) +} diff --git a/beacon_node/rest_api/src/metrics.rs b/beacon_node/http_metrics/src/metrics.rs similarity index 69% rename from beacon_node/rest_api/src/metrics.rs rename to beacon_node/http_metrics/src/metrics.rs index 4b1ba737d..bcd803c40 100644 --- a/beacon_node/rest_api/src/metrics.rs +++ b/beacon_node/http_metrics/src/metrics.rs @@ -1,38 +1,11 @@ -use crate::{ApiError, Context}; +use crate::Context; use beacon_chain::BeaconChainTypes; +use eth2::lighthouse::Health; use lighthouse_metrics::{Encoder, TextEncoder}; -use rest_types::Health; -use std::sync::Arc; pub use lighthouse_metrics::*; lazy_static! { - pub static ref BEACON_HTTP_API_REQUESTS_TOTAL: Result<IntCounterVec> = - try_create_int_counter_vec( - "beacon_http_api_requests_total", - "Count of HTTP requests received", - &["endpoint"] - ); - pub static ref BEACON_HTTP_API_SUCCESS_TOTAL: Result<IntCounterVec> = - try_create_int_counter_vec( - "beacon_http_api_success_total", - "Count of HTTP requests that returned 200 OK", - &["endpoint"] - ); - pub static ref BEACON_HTTP_API_ERROR_TOTAL: Result<IntCounterVec> = try_create_int_counter_vec( - "beacon_http_api_error_total", - "Count of HTTP that did not return 200 OK", - &["endpoint"] - ); - pub static ref BEACON_HTTP_API_TIMES_TOTAL: Result<HistogramVec> = try_create_histogram_vec( - "beacon_http_api_times_total", - "Duration to process HTTP requests", - &["endpoint"] - ); - pub static ref REQUEST_RESPONSE_TIME: Result<Histogram> = try_create_histogram( - "http_server_request_duration_seconds", - "Time taken to build a response to a HTTP request" - ); pub static ref PROCESS_NUM_THREADS: Result<IntGauge> = try_create_int_gauge( "process_num_threads", "Number of threads used by the current process" @@ -67,14 +40,9 @@ lazy_static! { try_create_float_gauge("system_loadavg_15", "Loadavg over 15 minutes"); } -/// Returns the full set of Prometheus metrics for the Beacon Node application. -/// -/// # Note -/// -/// This is a HTTP handler method. -pub fn get_prometheus<T: BeaconChainTypes>( - ctx: Arc<Context<T>>, -) -> std::result::Result<String, ApiError> { +pub fn gather_prometheus_metrics<T: BeaconChainTypes>( + ctx: &Context<T>, +) -> std::result::Result<String, String> { let mut buffer = vec![]; let encoder = TextEncoder::new(); @@ -94,9 +62,17 @@ pub fn get_prometheus<T: BeaconChainTypes>( // using `lighthouse_metrics::gather(..)` to collect the global `DEFAULT_REGISTRY` metrics into // a string that can be returned via HTTP. - slot_clock::scrape_for_metrics::<T::EthSpec, T::SlotClock>(&ctx.beacon_chain.slot_clock); - store::scrape_for_metrics(&ctx.db_path, &ctx.freezer_db_path); - beacon_chain::scrape_for_metrics(&ctx.beacon_chain); + if let Some(beacon_chain) = ctx.chain.as_ref() { + slot_clock::scrape_for_metrics::<T::EthSpec, T::SlotClock>(&beacon_chain.slot_clock); + beacon_chain::scrape_for_metrics(beacon_chain); + } + + if let (Some(db_path), Some(freezer_db_path)) = + (ctx.db_path.as_ref(), ctx.freezer_db_path.as_ref()) + { + store::scrape_for_metrics(db_path, freezer_db_path); + } + eth2_libp2p::scrape_discovery_metrics(); // This will silently fail if we are unable to observe the health. This is desired behaviour @@ -125,6 +101,5 @@ pub fn get_prometheus<T: BeaconChainTypes>( .encode(&lighthouse_metrics::gather(), &mut buffer) .unwrap(); - String::from_utf8(buffer) - .map_err(|e| ApiError::ServerError(format!("Failed to encode prometheus info: {:?}", e))) + String::from_utf8(buffer).map_err(|e| format!("Failed to encode prometheus info: {:?}", e)) } diff --git a/beacon_node/http_metrics/tests/tests.rs b/beacon_node/http_metrics/tests/tests.rs new file mode 100644 index 000000000..18a40d4f8 --- /dev/null +++ b/beacon_node/http_metrics/tests/tests.rs @@ -0,0 +1,46 @@ +use beacon_chain::test_utils::BlockingMigratorEphemeralHarnessType; +use environment::null_logger; +use http_metrics::Config; +use reqwest::StatusCode; +use std::net::Ipv4Addr; +use std::sync::Arc; +use tokio::sync::oneshot; +use types::MainnetEthSpec; + +type Context = http_metrics::Context<BlockingMigratorEphemeralHarnessType<MainnetEthSpec>>; + +#[tokio::test(core_threads = 2)] +async fn returns_200_ok() { + let log = null_logger().unwrap(); + + let context = Arc::new(Context { + config: Config { + enabled: true, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 0, + allow_origin: None, + }, + chain: None, + db_path: None, + freezer_db_path: None, + log, + }); + + let ctx = context.clone(); + let (_shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + let server_shutdown = async { + // It's not really interesting why this triggered, just that it happened. + let _ = shutdown_rx.await; + }; + let (listening_socket, server) = http_metrics::serve(ctx, server_shutdown).unwrap(); + + tokio::spawn(async { server.await }); + + let url = format!( + "http://{}:{}/metrics", + listening_socket.ip(), + listening_socket.port() + ); + + assert_eq!(reqwest::get(&url).await.unwrap().status(), StatusCode::OK); +} diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 0448e7762..ad856a6d2 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -17,7 +17,6 @@ beacon_chain = { path = "../beacon_chain" } store = { path = "../store" } eth2_libp2p = { path = "../eth2_libp2p" } hashset_delay = { path = "../../common/hashset_delay" } -rest_types = { path = "../../common/rest_types" } types = { path = "../../consensus/types" } state_processing = { path = "../../consensus/state_processing" } slot_clock = { path = "../../common/slot_clock" } diff --git a/beacon_node/network/src/attestation_service/mod.rs b/beacon_node/network/src/attestation_service/mod.rs index 59f63890a..7c017d295 100644 --- a/beacon_node/network/src/attestation_service/mod.rs +++ b/beacon_node/network/src/attestation_service/mod.rs @@ -15,9 +15,8 @@ use slog::{debug, error, o, trace, warn}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2_libp2p::SubnetDiscovery; use hashset_delay::HashSetDelay; -use rest_types::ValidatorSubscription; use slot_clock::SlotClock; -use types::{Attestation, EthSpec, Slot, SubnetId}; +use types::{Attestation, EthSpec, Slot, SubnetId, ValidatorSubscription}; use crate::metrics; diff --git a/beacon_node/network/src/beacon_processor/worker.rs b/beacon_node/network/src/beacon_processor/worker.rs index 653922dfe..1abb2a279 100644 --- a/beacon_node/network/src/beacon_processor/worker.rs +++ b/beacon_node/network/src/beacon_processor/worker.rs @@ -45,7 +45,7 @@ impl<T: BeaconChainTypes> Worker<T> { let attestation = match self .chain - .verify_unaggregated_attestation_for_gossip(attestation, subnet_id) + .verify_unaggregated_attestation_for_gossip(attestation, Some(subnet_id)) { Ok(attestation) => attestation, Err(e) => { diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 1147562b4..1f9ddd6a0 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -15,13 +15,12 @@ use eth2_libp2p::{ }; use eth2_libp2p::{MessageAcceptance, Service as LibP2PService}; use futures::prelude::*; -use rest_types::ValidatorSubscription; use slog::{debug, error, info, o, trace, warn}; use std::{collections::HashMap, sync::Arc, time::Duration}; use store::HotColdDB; use tokio::sync::mpsc; use tokio::time::Delay; -use types::EthSpec; +use types::{EthSpec, ValidatorSubscription}; mod tests; diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 5b664c877..6d6a8d1cd 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -332,6 +332,51 @@ impl<T: EthSpec> OperationPool<T> { pub fn num_voluntary_exits(&self) -> usize { self.voluntary_exits.read().len() } + + /// Returns all known `Attestation` objects. + /// + /// This method may return objects that are invalid for block inclusion. + pub fn get_all_attestations(&self) -> Vec<Attestation<T>> { + self.attestations + .read() + .iter() + .map(|(_, attns)| attns.iter().cloned()) + .flatten() + .collect() + } + + /// Returns all known `AttesterSlashing` objects. + /// + /// This method may return objects that are invalid for block inclusion. + pub fn get_all_attester_slashings(&self) -> Vec<AttesterSlashing<T>> { + self.attester_slashings + .read() + .iter() + .map(|(slashing, _)| slashing.clone()) + .collect() + } + + /// Returns all known `ProposerSlashing` objects. + /// + /// This method may return objects that are invalid for block inclusion. + pub fn get_all_proposer_slashings(&self) -> Vec<ProposerSlashing> { + self.proposer_slashings + .read() + .iter() + .map(|(_, slashing)| slashing.clone()) + .collect() + } + + /// Returns all known `SignedVoluntaryExit` objects. + /// + /// This method may return objects that are invalid for block inclusion. + pub fn get_all_voluntary_exits(&self) -> Vec<SignedVoluntaryExit> { + self.voluntary_exits + .read() + .iter() + .map(|(_, exit)| exit.clone()) + .collect() + } } /// Filter up to a maximum number of operations out of an iterator. diff --git a/beacon_node/rest_api/src/beacon.rs b/beacon_node/rest_api/src/beacon.rs deleted file mode 100644 index ad2688bb0..000000000 --- a/beacon_node/rest_api/src/beacon.rs +++ /dev/null @@ -1,499 +0,0 @@ -use crate::helpers::*; -use crate::validator::get_state_for_epoch; -use crate::Context; -use crate::{ApiError, UrlQuery}; -use beacon_chain::{ - observed_operations::ObservationOutcome, BeaconChain, BeaconChainTypes, StateSkipConfig, -}; -use futures::executor::block_on; -use hyper::body::Bytes; -use hyper::{Body, Request}; -use rest_types::{ - BlockResponse, CanonicalHeadResponse, Committee, HeadBeaconBlock, StateResponse, - ValidatorRequest, ValidatorResponse, -}; -use std::io::Write; -use std::sync::Arc; - -use slog::error; -use types::{ - AttesterSlashing, BeaconState, EthSpec, Hash256, ProposerSlashing, PublicKeyBytes, - RelativeEpoch, SignedBeaconBlockHash, Slot, -}; - -/// Returns a summary of the head of the beacon chain. -pub fn get_head<T: BeaconChainTypes>( - ctx: Arc<Context<T>>, -) -> Result<CanonicalHeadResponse, ApiError> { - let beacon_chain = &ctx.beacon_chain; - let chain_head = beacon_chain.head()?; - - Ok(CanonicalHeadResponse { - slot: chain_head.beacon_state.slot, - block_root: chain_head.beacon_block_root, - state_root: chain_head.beacon_state_root, - finalized_slot: chain_head - .beacon_state - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - finalized_block_root: chain_head.beacon_state.finalized_checkpoint.root, - justified_slot: chain_head - .beacon_state - .current_justified_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - justified_block_root: chain_head.beacon_state.current_justified_checkpoint.root, - previous_justified_slot: chain_head - .beacon_state - .previous_justified_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - previous_justified_block_root: chain_head.beacon_state.previous_justified_checkpoint.root, - }) -} - -/// Return the list of heads of the beacon chain. -pub fn get_heads<T: BeaconChainTypes>(ctx: Arc<Context<T>>) -> Vec<HeadBeaconBlock> { - ctx.beacon_chain - .heads() - .into_iter() - .map(|(beacon_block_root, beacon_block_slot)| HeadBeaconBlock { - beacon_block_root, - beacon_block_slot, - }) - .collect() -} - -/// HTTP handler to return a `BeaconBlock` at a given `root` or `slot`. -pub fn get_block<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<BlockResponse<T::EthSpec>, ApiError> { - let beacon_chain = &ctx.beacon_chain; - let query_params = ["root", "slot"]; - let (key, value) = UrlQuery::from_request(&req)?.first_of(&query_params)?; - - let block_root = match (key.as_ref(), value) { - ("slot", value) => { - let target = parse_slot(&value)?; - - block_root_at_slot(beacon_chain, target)?.ok_or_else(|| { - ApiError::NotFound(format!( - "Unable to find SignedBeaconBlock for slot {:?}", - target - )) - })? - } - ("root", value) => parse_root(&value)?, - _ => return Err(ApiError::ServerError("Unexpected query parameter".into())), - }; - - let block = beacon_chain.store.get_block(&block_root)?.ok_or_else(|| { - ApiError::NotFound(format!( - "Unable to find SignedBeaconBlock for root {:?}", - block_root - )) - })?; - - Ok(BlockResponse { - root: block_root, - beacon_block: block, - }) -} - -/// HTTP handler to return a `SignedBeaconBlock` root at a given `slot`. -pub fn get_block_root<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Hash256, ApiError> { - let slot_string = UrlQuery::from_request(&req)?.only_one("slot")?; - let target = parse_slot(&slot_string)?; - - block_root_at_slot(&ctx.beacon_chain, target)?.ok_or_else(|| { - ApiError::NotFound(format!( - "Unable to find SignedBeaconBlock for slot {:?}", - target - )) - }) -} - -fn make_sse_response_chunk(new_head_hash: SignedBeaconBlockHash) -> std::io::Result<Bytes> { - let mut buffer = Vec::new(); - { - let mut sse_message = uhttp_sse::SseMessage::new(&mut buffer); - let untyped_hash: Hash256 = new_head_hash.into(); - write!(sse_message.data()?, "{:?}", untyped_hash)?; - } - let bytes: Bytes = buffer.into(); - Ok(bytes) -} - -pub fn stream_forks<T: BeaconChainTypes>(ctx: Arc<Context<T>>) -> Result<Body, ApiError> { - let mut events = ctx.events.lock().add_rx(); - let (mut sender, body) = Body::channel(); - std::thread::spawn(move || { - while let Ok(new_head_hash) = events.recv() { - let chunk = match make_sse_response_chunk(new_head_hash) { - Ok(chunk) => chunk, - Err(e) => { - error!(ctx.log, "Failed to make SSE chunk"; "error" => e.to_string()); - sender.abort(); - break; - } - }; - match block_on(sender.send_data(chunk)) { - Err(e) if e.is_closed() => break, - Err(e) => error!(ctx.log, "Couldn't stream piece {:?}", e), - Ok(_) => (), - } - } - }); - Ok(body) -} - -/// HTTP handler to which accepts a query string of a list of validator pubkeys and maps it to a -/// `ValidatorResponse`. -/// -/// This method is limited to as many `pubkeys` that can fit in a URL. See `post_validators` for -/// doing bulk requests. -pub fn get_validators<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorResponse>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let validator_pubkeys = query - .all_of("validator_pubkeys")? - .iter() - .map(|validator_pubkey_str| parse_pubkey_bytes(validator_pubkey_str)) - .collect::<Result<Vec<_>, _>>()?; - - let state_root_opt = if let Some((_key, value)) = query.first_of_opt(&["state_root"]) { - Some(parse_root(&value)?) - } else { - None - }; - - validator_responses_by_pubkey(&ctx.beacon_chain, state_root_opt, validator_pubkeys) -} - -/// HTTP handler to return all validators, each as a `ValidatorResponse`. -pub fn get_all_validators<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorResponse>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let state_root_opt = if let Some((_key, value)) = query.first_of_opt(&["state_root"]) { - Some(parse_root(&value)?) - } else { - None - }; - - let mut state = get_state_from_root_opt(&ctx.beacon_chain, state_root_opt)?; - - let validators = state.validators.clone(); - validators - .iter() - .map(|validator| validator_response_by_pubkey(&mut state, validator.pubkey.clone())) - .collect::<Result<Vec<_>, _>>() -} - -/// HTTP handler to return all active validators, each as a `ValidatorResponse`. -pub fn get_active_validators<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorResponse>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let state_root_opt = if let Some((_key, value)) = query.first_of_opt(&["state_root"]) { - Some(parse_root(&value)?) - } else { - None - }; - - let mut state = get_state_from_root_opt(&ctx.beacon_chain, state_root_opt)?; - - let validators = state.validators.clone(); - let current_epoch = state.current_epoch(); - - validators - .iter() - .filter(|validator| validator.is_active_at(current_epoch)) - .map(|validator| validator_response_by_pubkey(&mut state, validator.pubkey.clone())) - .collect::<Result<Vec<_>, _>>() -} - -/// HTTP handler to which accepts a `ValidatorRequest` and returns a `ValidatorResponse` for -/// each of the given `pubkeys`. When `state_root` is `None`, the canonical head is used. -/// -/// This method allows for a basically unbounded list of `pubkeys`, where as the `get_validators` -/// request is limited by the max number of pubkeys you can fit in a URL. -pub fn post_validators<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorResponse>, ApiError> { - serde_json::from_slice::<ValidatorRequest>(&req.into_body()) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to parse JSON into ValidatorRequest: {:?}", - e - )) - }) - .and_then(|bulk_request| { - validator_responses_by_pubkey( - &ctx.beacon_chain, - bulk_request.state_root, - bulk_request.pubkeys, - ) - }) -} - -/// Returns either the state given by `state_root_opt`, or the canonical head state if it is -/// `None`. -fn get_state_from_root_opt<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - state_root_opt: Option<Hash256>, -) -> Result<BeaconState<T::EthSpec>, ApiError> { - if let Some(state_root) = state_root_opt { - beacon_chain - .get_state(&state_root, None) - .map_err(|e| { - ApiError::ServerError(format!( - "Database error when reading state root {}: {:?}", - state_root, e - )) - })? - .ok_or_else(|| ApiError::NotFound(format!("No state exists with root: {}", state_root))) - } else { - Ok(beacon_chain.head()?.beacon_state) - } -} - -/// Maps a vec of `validator_pubkey` to a vec of `ValidatorResponse`, using the state at the given -/// `state_root`. If `state_root.is_none()`, uses the canonial head state. -fn validator_responses_by_pubkey<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - state_root_opt: Option<Hash256>, - validator_pubkeys: Vec<PublicKeyBytes>, -) -> Result<Vec<ValidatorResponse>, ApiError> { - let mut state = get_state_from_root_opt(beacon_chain, state_root_opt)?; - - validator_pubkeys - .into_iter() - .map(|validator_pubkey| validator_response_by_pubkey(&mut state, validator_pubkey)) - .collect::<Result<Vec<_>, ApiError>>() -} - -/// Maps a `validator_pubkey` to a `ValidatorResponse`, using the given state. -/// -/// The provided `state` must have a fully up-to-date pubkey cache. -fn validator_response_by_pubkey<E: EthSpec>( - state: &mut BeaconState<E>, - validator_pubkey: PublicKeyBytes, -) -> Result<ValidatorResponse, ApiError> { - let validator_index_opt = state - .get_validator_index(&validator_pubkey) - .map_err(|e| ApiError::ServerError(format!("Unable to read pubkey cache: {:?}", e)))?; - - if let Some(validator_index) = validator_index_opt { - let balance = state.balances.get(validator_index).ok_or_else(|| { - ApiError::ServerError(format!("Invalid balances index: {:?}", validator_index)) - })?; - - let validator = state - .validators - .get(validator_index) - .ok_or_else(|| { - ApiError::ServerError(format!("Invalid validator index: {:?}", validator_index)) - })? - .clone(); - - Ok(ValidatorResponse { - pubkey: validator_pubkey, - validator_index: Some(validator_index), - balance: Some(*balance), - validator: Some(validator), - }) - } else { - Ok(ValidatorResponse { - pubkey: validator_pubkey, - validator_index: None, - balance: None, - validator: None, - }) - } -} - -/// HTTP handler -pub fn get_committees<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<Committee>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let epoch = query.epoch()?; - - let mut state = - get_state_for_epoch(&ctx.beacon_chain, epoch, StateSkipConfig::WithoutStateRoots)?; - - let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch).map_err(|e| { - ApiError::ServerError(format!("Failed to get state suitable for epoch: {:?}", e)) - })?; - - state - .build_committee_cache(relative_epoch, &ctx.beacon_chain.spec) - .map_err(|e| ApiError::ServerError(format!("Unable to build committee cache: {:?}", e)))?; - - Ok(state - .get_beacon_committees_at_epoch(relative_epoch) - .map_err(|e| ApiError::ServerError(format!("Unable to get all committees: {:?}", e)))? - .into_iter() - .map(|c| Committee { - slot: c.slot, - index: c.index, - committee: c.committee.to_vec(), - }) - .collect::<Vec<_>>()) -} - -/// HTTP handler to return a `BeaconState` at a given `root` or `slot`. -/// -/// Will not return a state if the request slot is in the future. Will return states higher than -/// the current head by skipping slots. -pub fn get_state<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<StateResponse<T::EthSpec>, ApiError> { - let head_state = ctx.beacon_chain.head()?.beacon_state; - - let (key, value) = match UrlQuery::from_request(&req) { - Ok(query) => { - // We have *some* parameters, just check them. - let query_params = ["root", "slot"]; - query.first_of(&query_params)? - } - Err(ApiError::BadRequest(_)) => { - // No parameters provided at all, use current slot. - (String::from("slot"), head_state.slot.to_string()) - } - Err(e) => { - return Err(e); - } - }; - - let (root, state): (Hash256, BeaconState<T::EthSpec>) = match (key.as_ref(), value) { - ("slot", value) => state_at_slot(&ctx.beacon_chain, parse_slot(&value)?)?, - ("root", value) => { - let root = &parse_root(&value)?; - - let state = ctx - .beacon_chain - .store - .get_state(root, None)? - .ok_or_else(|| ApiError::NotFound(format!("No state for root: {:?}", root)))?; - - (*root, state) - } - _ => return Err(ApiError::ServerError("Unexpected query parameter".into())), - }; - - Ok(StateResponse { - root, - beacon_state: state, - }) -} - -/// HTTP handler to return a `BeaconState` root at a given `slot`. -/// -/// Will not return a state if the request slot is in the future. Will return states higher than -/// the current head by skipping slots. -pub fn get_state_root<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Hash256, ApiError> { - let slot_string = UrlQuery::from_request(&req)?.only_one("slot")?; - let slot = parse_slot(&slot_string)?; - - state_root_at_slot(&ctx.beacon_chain, slot, StateSkipConfig::WithStateRoots) -} - -/// HTTP handler to return a `BeaconState` at the genesis block. -/// -/// This is an undocumented convenience method used during testing. For production, simply do a -/// state request at slot 0. -pub fn get_genesis_state<T: BeaconChainTypes>( - ctx: Arc<Context<T>>, -) -> Result<BeaconState<T::EthSpec>, ApiError> { - state_at_slot(&ctx.beacon_chain, Slot::new(0)).map(|(_root, state)| state) -} - -pub fn proposer_slashing<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<bool, ApiError> { - let body = req.into_body(); - - serde_json::from_slice::<ProposerSlashing>(&body) - .map_err(|e| format!("Unable to parse JSON into ProposerSlashing: {:?}", e)) - .and_then(move |proposer_slashing| { - if ctx.beacon_chain.eth1_chain.is_some() { - let obs_outcome = ctx - .beacon_chain - .verify_proposer_slashing_for_gossip(proposer_slashing) - .map_err(|e| format!("Error while verifying proposer slashing: {:?}", e))?; - if let ObservationOutcome::New(verified_proposer_slashing) = obs_outcome { - ctx.beacon_chain - .import_proposer_slashing(verified_proposer_slashing); - Ok(()) - } else { - Err("Proposer slashing for that validator index already known".into()) - } - } else { - Err("Cannot insert proposer slashing on node without Eth1 connection.".to_string()) - } - }) - .map_err(ApiError::BadRequest)?; - - Ok(true) -} - -pub fn attester_slashing<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<bool, ApiError> { - let body = req.into_body(); - serde_json::from_slice::<AttesterSlashing<T::EthSpec>>(&body) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to parse JSON into AttesterSlashing: {:?}", - e - )) - }) - .and_then(move |attester_slashing| { - if ctx.beacon_chain.eth1_chain.is_some() { - ctx.beacon_chain - .verify_attester_slashing_for_gossip(attester_slashing) - .map_err(|e| format!("Error while verifying attester slashing: {:?}", e)) - .and_then(|outcome| { - if let ObservationOutcome::New(verified_attester_slashing) = outcome { - ctx.beacon_chain - .import_attester_slashing(verified_attester_slashing) - .map_err(|e| { - format!("Error while importing attester slashing: {:?}", e) - }) - } else { - Err("Attester slashing only covers already slashed indices".to_string()) - } - }) - .map_err(ApiError::BadRequest) - } else { - Err(ApiError::BadRequest( - "Cannot insert attester slashing on node without Eth1 connection.".to_string(), - )) - } - })?; - - Ok(true) -} diff --git a/beacon_node/rest_api/src/config.rs b/beacon_node/rest_api/src/config.rs deleted file mode 100644 index 815fccfd0..000000000 --- a/beacon_node/rest_api/src/config.rs +++ /dev/null @@ -1,55 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::net::Ipv4Addr; - -/// Defines the encoding for the API. -#[derive(Clone, Serialize, Deserialize, Copy)] -pub enum ApiEncodingFormat { - JSON, - YAML, - SSZ, -} - -impl ApiEncodingFormat { - pub fn get_content_type(&self) -> &str { - match self { - ApiEncodingFormat::JSON => "application/json", - ApiEncodingFormat::YAML => "application/yaml", - ApiEncodingFormat::SSZ => "application/ssz", - } - } -} - -impl From<&str> for ApiEncodingFormat { - fn from(f: &str) -> ApiEncodingFormat { - match f { - "application/yaml" => ApiEncodingFormat::YAML, - "application/ssz" => ApiEncodingFormat::SSZ, - _ => ApiEncodingFormat::JSON, - } - } -} - -/// HTTP REST API Configuration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - /// Enable the REST API server. - pub enabled: bool, - /// The IPv4 address the REST API HTTP server will listen on. - pub listen_address: Ipv4Addr, - /// The port the REST API HTTP server will listen on. - pub port: u16, - /// If something else than "", a 'Access-Control-Allow-Origin' header will be present in - /// responses. Put *, to allow any origin. - pub allow_origin: String, -} - -impl Default for Config { - fn default() -> Self { - Config { - enabled: false, - listen_address: Ipv4Addr::new(127, 0, 0, 1), - port: 5052, - allow_origin: "".to_string(), - } - } -} diff --git a/beacon_node/rest_api/src/consensus.rs b/beacon_node/rest_api/src/consensus.rs deleted file mode 100644 index 9df57f055..000000000 --- a/beacon_node/rest_api/src/consensus.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::helpers::*; -use crate::{ApiError, Context, UrlQuery}; -use beacon_chain::BeaconChainTypes; -use hyper::Request; -use rest_types::{IndividualVotesRequest, IndividualVotesResponse}; -use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_epoch_processing::{TotalBalances, ValidatorStatuses}; -use std::sync::Arc; -use types::EthSpec; - -/// The results of validators voting during an epoch. -/// -/// Provides information about the current and previous epochs. -#[derive(Serialize, Deserialize, Encode, Decode)] -pub struct VoteCount { - /// The total effective balance of all active validators during the _current_ epoch. - pub current_epoch_active_gwei: u64, - /// The total effective balance of all active validators during the _previous_ epoch. - pub previous_epoch_active_gwei: u64, - /// The total effective balance of all validators who attested during the _current_ epoch. - pub current_epoch_attesting_gwei: u64, - /// The total effective balance of all validators who attested during the _current_ epoch and - /// agreed with the state about the beacon block at the first slot of the _current_ epoch. - pub current_epoch_target_attesting_gwei: u64, - /// The total effective balance of all validators who attested during the _previous_ epoch. - pub previous_epoch_attesting_gwei: u64, - /// The total effective balance of all validators who attested during the _previous_ epoch and - /// agreed with the state about the beacon block at the first slot of the _previous_ epoch. - pub previous_epoch_target_attesting_gwei: u64, - /// The total effective balance of all validators who attested during the _previous_ epoch and - /// agreed with the state about the beacon block at the time of attestation. - pub previous_epoch_head_attesting_gwei: u64, -} - -impl Into<VoteCount> for TotalBalances { - fn into(self) -> VoteCount { - VoteCount { - current_epoch_active_gwei: self.current_epoch(), - previous_epoch_active_gwei: self.previous_epoch(), - current_epoch_attesting_gwei: self.current_epoch_attesters(), - current_epoch_target_attesting_gwei: self.current_epoch_target_attesters(), - previous_epoch_attesting_gwei: self.previous_epoch_attesters(), - previous_epoch_target_attesting_gwei: self.previous_epoch_target_attesters(), - previous_epoch_head_attesting_gwei: self.previous_epoch_head_attesters(), - } - } -} - -/// HTTP handler return a `VoteCount` for some given `Epoch`. -pub fn get_vote_count<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<VoteCount, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let epoch = query.epoch()?; - // This is the last slot of the given epoch (one prior to the first slot of the next epoch). - let target_slot = (epoch + 1).start_slot(T::EthSpec::slots_per_epoch()) - 1; - - let (_root, state) = state_at_slot(&ctx.beacon_chain, target_slot)?; - let spec = &ctx.beacon_chain.spec; - - let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; - validator_statuses.process_attestations(&state, spec)?; - - Ok(validator_statuses.total_balances.into()) -} - -pub fn post_individual_votes<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<IndividualVotesResponse>, ApiError> { - let body = req.into_body(); - - serde_json::from_slice::<IndividualVotesRequest>(&body) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to parse JSON into ValidatorDutiesRequest: {:?}", - e - )) - }) - .and_then(move |body| { - let epoch = body.epoch; - - // This is the last slot of the given epoch (one prior to the first slot of the next epoch). - let target_slot = (epoch + 1).start_slot(T::EthSpec::slots_per_epoch()) - 1; - - let (_root, mut state) = state_at_slot(&ctx.beacon_chain, target_slot)?; - let spec = &ctx.beacon_chain.spec; - - let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; - validator_statuses.process_attestations(&state, spec)?; - - body.pubkeys - .into_iter() - .map(|pubkey| { - let validator_index_opt = state.get_validator_index(&pubkey).map_err(|e| { - ApiError::ServerError(format!("Unable to read pubkey cache: {:?}", e)) - })?; - - if let Some(validator_index) = validator_index_opt { - let vote = validator_statuses - .statuses - .get(validator_index) - .cloned() - .map(Into::into); - - Ok(IndividualVotesResponse { - epoch, - pubkey, - validator_index: Some(validator_index), - vote, - }) - } else { - Ok(IndividualVotesResponse { - epoch, - pubkey, - validator_index: None, - vote: None, - }) - } - }) - .collect::<Result<Vec<_>, _>>() - }) -} diff --git a/beacon_node/rest_api/src/helpers.rs b/beacon_node/rest_api/src/helpers.rs deleted file mode 100644 index 66b5bd1a0..000000000 --- a/beacon_node/rest_api/src/helpers.rs +++ /dev/null @@ -1,260 +0,0 @@ -use crate::{ApiError, NetworkChannel}; -use beacon_chain::{BeaconChain, BeaconChainTypes, StateSkipConfig}; -use bls::PublicKeyBytes; -use eth2_libp2p::PubsubMessage; -use itertools::process_results; -use network::NetworkMessage; -use ssz::Decode; -use store::iter::AncestorIter; -use types::{ - BeaconState, CommitteeIndex, Epoch, EthSpec, Hash256, RelativeEpoch, SignedBeaconBlock, Slot, -}; - -/// Parse a slot. -/// -/// E.g., `"1234"` -pub fn parse_slot(string: &str) -> Result<Slot, ApiError> { - string - .parse::<u64>() - .map(Slot::from) - .map_err(|e| ApiError::BadRequest(format!("Unable to parse slot: {:?}", e))) -} - -/// Parse an epoch. -/// -/// E.g., `"13"` -pub fn parse_epoch(string: &str) -> Result<Epoch, ApiError> { - string - .parse::<u64>() - .map(Epoch::from) - .map_err(|e| ApiError::BadRequest(format!("Unable to parse epoch: {:?}", e))) -} - -/// Parse a CommitteeIndex. -/// -/// E.g., `"18"` -pub fn parse_committee_index(string: &str) -> Result<CommitteeIndex, ApiError> { - string - .parse::<CommitteeIndex>() - .map_err(|e| ApiError::BadRequest(format!("Unable to parse committee index: {:?}", e))) -} - -/// Parse an SSZ object from some hex-encoded bytes. -/// -/// E.g., A signature is `"0x0000000000000000000000000000000000000000000000000000000000000000"` -pub fn parse_hex_ssz_bytes<T: Decode>(string: &str) -> Result<T, ApiError> { - const PREFIX: &str = "0x"; - - if string.starts_with(PREFIX) { - let trimmed = string.trim_start_matches(PREFIX); - let bytes = hex::decode(trimmed) - .map_err(|e| ApiError::BadRequest(format!("Unable to parse SSZ hex: {:?}", e)))?; - T::from_ssz_bytes(&bytes) - .map_err(|e| ApiError::BadRequest(format!("Unable to parse SSZ bytes: {:?}", e))) - } else { - Err(ApiError::BadRequest( - "Hex bytes must have a 0x prefix".to_string(), - )) - } -} - -/// Parse a root from a `0x` prefixed string. -/// -/// E.g., `"0x0000000000000000000000000000000000000000000000000000000000000000"` -pub fn parse_root(string: &str) -> Result<Hash256, ApiError> { - const PREFIX: &str = "0x"; - - if string.starts_with(PREFIX) { - let trimmed = string.trim_start_matches(PREFIX); - trimmed - .parse() - .map_err(|e| ApiError::BadRequest(format!("Unable to parse root: {:?}", e))) - } else { - Err(ApiError::BadRequest( - "Root must have a 0x prefix".to_string(), - )) - } -} - -/// Parse a PublicKey from a `0x` prefixed hex string -pub fn parse_pubkey_bytes(string: &str) -> Result<PublicKeyBytes, ApiError> { - const PREFIX: &str = "0x"; - if string.starts_with(PREFIX) { - let pubkey_bytes = hex::decode(string.trim_start_matches(PREFIX)) - .map_err(|e| ApiError::BadRequest(format!("Invalid hex string: {:?}", e)))?; - let pubkey = PublicKeyBytes::deserialize(pubkey_bytes.as_slice()).map_err(|e| { - ApiError::BadRequest(format!("Unable to deserialize public key: {:?}.", e)) - })?; - Ok(pubkey) - } else { - Err(ApiError::BadRequest( - "Public key must have a 0x prefix".to_string(), - )) - } -} - -/// Returns the root of the `SignedBeaconBlock` in the canonical chain of `beacon_chain` at the given -/// `slot`, if possible. -/// -/// May return a root for a previous slot, in the case of skip slots. -pub fn block_root_at_slot<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - target: Slot, -) -> Result<Option<Hash256>, ApiError> { - Ok(process_results( - beacon_chain.rev_iter_block_roots()?, - |iter| { - iter.take_while(|(_, slot)| *slot >= target) - .find(|(_, slot)| *slot == target) - .map(|(root, _)| root) - }, - )?) -} - -/// Returns a `BeaconState` and it's root in the canonical chain of `beacon_chain` at the given -/// `slot`, if possible. -/// -/// Will not return a state if the request slot is in the future. Will return states higher than -/// the current head by skipping slots. -pub fn state_at_slot<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - slot: Slot, -) -> Result<(Hash256, BeaconState<T::EthSpec>), ApiError> { - let head = beacon_chain.head()?; - - if head.beacon_state.slot == slot { - Ok((head.beacon_state_root, head.beacon_state)) - } else { - let root = state_root_at_slot(beacon_chain, slot, StateSkipConfig::WithStateRoots)?; - - let state: BeaconState<T::EthSpec> = beacon_chain - .store - .get_state(&root, Some(slot))? - .ok_or_else(|| ApiError::NotFound(format!("Unable to find state at root {}", root)))?; - - Ok((root, state)) - } -} - -/// Returns the root of the `BeaconState` in the canonical chain of `beacon_chain` at the given -/// `slot`, if possible. -/// -/// Will not return a state root if the request slot is in the future. Will return state roots -/// higher than the current head by skipping slots. -pub fn state_root_at_slot<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - slot: Slot, - config: StateSkipConfig, -) -> Result<Hash256, ApiError> { - let head_state = &beacon_chain.head()?.beacon_state; - let current_slot = beacon_chain - .slot() - .map_err(|_| ApiError::ServerError("Unable to read slot clock".to_string()))?; - - // There are four scenarios when obtaining a state for a given slot: - // - // 1. The request slot is in the future. - // 2. The request slot is the same as the best block (head) slot. - // 3. The request slot is prior to the head slot. - // 4. The request slot is later than the head slot. - if current_slot < slot { - // 1. The request slot is in the future. Reject the request. - // - // We could actually speculate about future state roots by skipping slots, however that's - // likely to cause confusion for API users. - Err(ApiError::BadRequest(format!( - "Requested slot {} is past the current slot {}", - slot, current_slot - ))) - } else if head_state.slot == slot { - // 2. The request slot is the same as the best block (head) slot. - // - // The head state root is stored in memory, return a reference. - Ok(beacon_chain.head()?.beacon_state_root) - } else if head_state.slot > slot { - // 3. The request slot is prior to the head slot. - // - // Iterate through the state roots on the head state to find the root for that - // slot. Once the root is found, load it from the database. - process_results( - head_state - .try_iter_ancestor_roots(beacon_chain.store.clone()) - .ok_or_else(|| { - ApiError::ServerError("Failed to create roots iterator".to_string()) - })?, - |mut iter| iter.find(|(_, s)| *s == slot).map(|(root, _)| root), - )? - .ok_or_else(|| ApiError::NotFound(format!("Unable to find state at slot {}", slot))) - } else { - // 4. The request slot is later than the head slot. - // - // Use `per_slot_processing` to advance the head state to the present slot, - // assuming that all slots do not contain a block (i.e., they are skipped slots). - let mut state = beacon_chain.head()?.beacon_state; - let spec = &T::EthSpec::default_spec(); - - let skip_state_root = match config { - StateSkipConfig::WithStateRoots => None, - StateSkipConfig::WithoutStateRoots => Some(Hash256::zero()), - }; - - for _ in state.slot.as_u64()..slot.as_u64() { - // Ensure the next epoch state caches are built in case of an epoch transition. - state.build_committee_cache(RelativeEpoch::Next, spec)?; - - state_processing::per_slot_processing(&mut state, skip_state_root, spec)?; - } - - // Note: this is an expensive operation. Once the tree hash cache is implement it may be - // used here. - Ok(state.canonical_root()) - } -} - -pub fn publish_beacon_block_to_network<T: BeaconChainTypes + 'static>( - chan: &NetworkChannel<T::EthSpec>, - block: SignedBeaconBlock<T::EthSpec>, -) -> Result<(), ApiError> { - // send the block via SSZ encoding - let messages = vec![PubsubMessage::BeaconBlock(Box::new(block))]; - - // Publish the block to the p2p network via gossipsub. - if let Err(e) = chan.send(NetworkMessage::Publish { messages }) { - return Err(ApiError::ServerError(format!( - "Unable to send new block to network: {:?}", - e - ))); - } - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn parse_root_works() { - assert_eq!( - parse_root("0x0000000000000000000000000000000000000000000000000000000000000000"), - Ok(Hash256::zero()) - ); - assert_eq!( - parse_root("0x000000000000000000000000000000000000000000000000000000000000002a"), - Ok(Hash256::from_low_u64_be(42)) - ); - assert!( - parse_root("0000000000000000000000000000000000000000000000000000000000000042").is_err() - ); - assert!(parse_root("0x").is_err()); - assert!(parse_root("0x00").is_err()); - } - - #[test] - fn parse_slot_works() { - assert_eq!(parse_slot("0"), Ok(Slot::new(0))); - assert_eq!(parse_slot("42"), Ok(Slot::new(42))); - assert_eq!(parse_slot("10000000"), Ok(Slot::new(10_000_000))); - assert!(parse_slot("cats").is_err()); - } -} diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs deleted file mode 100644 index 405e08e21..000000000 --- a/beacon_node/rest_api/src/lib.rs +++ /dev/null @@ -1,127 +0,0 @@ -#[macro_use] -extern crate lazy_static; -mod router; -extern crate network as client_network; - -mod beacon; -pub mod config; -mod consensus; -mod helpers; -mod lighthouse; -mod metrics; -mod node; -mod url_query; -mod validator; - -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use bus::Bus; -use client_network::NetworkMessage; -pub use config::ApiEncodingFormat; -use eth2_config::Eth2Config; -use eth2_libp2p::NetworkGlobals; -use futures::future::TryFutureExt; -use hyper::server::conn::AddrStream; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Request, Server}; -use parking_lot::Mutex; -use rest_types::ApiError; -use slog::{info, warn}; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::mpsc; -use types::SignedBeaconBlockHash; -use url_query::UrlQuery; - -pub use crate::helpers::parse_pubkey_bytes; -pub use config::Config; -pub use router::Context; - -pub type NetworkChannel<T> = mpsc::UnboundedSender<NetworkMessage<T>>; - -pub struct NetworkInfo<T: BeaconChainTypes> { - pub network_globals: Arc<NetworkGlobals<T::EthSpec>>, - pub network_chan: NetworkChannel<T::EthSpec>, -} - -// Allowing more than 7 arguments. -#[allow(clippy::too_many_arguments)] -pub fn start_server<T: BeaconChainTypes>( - executor: environment::TaskExecutor, - config: &Config, - beacon_chain: Arc<BeaconChain<T>>, - network_info: NetworkInfo<T>, - db_path: PathBuf, - freezer_db_path: PathBuf, - eth2_config: Eth2Config, - events: Arc<Mutex<Bus<SignedBeaconBlockHash>>>, -) -> Result<SocketAddr, hyper::Error> { - let log = executor.log(); - let eth2_config = Arc::new(eth2_config); - - let context = Arc::new(Context { - executor: executor.clone(), - config: config.clone(), - beacon_chain, - network_globals: network_info.network_globals.clone(), - network_chan: network_info.network_chan, - eth2_config, - log: log.clone(), - db_path, - freezer_db_path, - events, - }); - - // Define the function that will build the request handler. - let make_service = make_service_fn(move |_socket: &AddrStream| { - let ctx = context.clone(); - - async move { - Ok::<_, hyper::Error>(service_fn(move |req: Request<Body>| { - router::on_http_request(req, ctx.clone()) - })) - } - }); - - let bind_addr = (config.listen_address, config.port).into(); - let server = Server::bind(&bind_addr).serve(make_service); - - // Determine the address the server is actually listening on. - // - // This may be different to `bind_addr` if bind port was 0 (this allows the OS to choose a free - // port). - let actual_listen_addr = server.local_addr(); - - // Build a channel to kill the HTTP server. - let exit = executor.exit(); - let inner_log = log.clone(); - let server_exit = async move { - let _ = exit.await; - info!(inner_log, "HTTP service shutdown"); - }; - - // Configure the `hyper` server to gracefully shutdown when the shutdown channel is triggered. - let inner_log = log.clone(); - let server_future = server - .with_graceful_shutdown(async { - server_exit.await; - }) - .map_err(move |e| { - warn!( - inner_log, - "HTTP server failed to start, Unable to bind"; "address" => format!("{:?}", e) - ) - }) - .unwrap_or_else(|_| ()); - - info!( - log, - "HTTP API started"; - "address" => format!("{}", actual_listen_addr.ip()), - "port" => actual_listen_addr.port(), - ); - - executor.spawn_without_exit(server_future, "http"); - - Ok(actual_listen_addr) -} diff --git a/beacon_node/rest_api/src/lighthouse.rs b/beacon_node/rest_api/src/lighthouse.rs deleted file mode 100644 index 4d0fae926..000000000 --- a/beacon_node/rest_api/src/lighthouse.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! This contains a collection of lighthouse specific HTTP endpoints. - -use crate::{ApiError, Context}; -use beacon_chain::BeaconChainTypes; -use eth2_libp2p::PeerInfo; -use serde::Serialize; -use std::sync::Arc; -use types::EthSpec; - -/// Returns all known peers and corresponding information -pub fn peers<T: BeaconChainTypes>(ctx: Arc<Context<T>>) -> Result<Vec<Peer<T::EthSpec>>, ApiError> { - Ok(ctx - .network_globals - .peers - .read() - .peers() - .map(|(peer_id, peer_info)| Peer { - peer_id: peer_id.to_string(), - peer_info: peer_info.clone(), - }) - .collect()) -} - -/// Returns all known connected peers and their corresponding information -pub fn connected_peers<T: BeaconChainTypes>( - ctx: Arc<Context<T>>, -) -> Result<Vec<Peer<T::EthSpec>>, ApiError> { - Ok(ctx - .network_globals - .peers - .read() - .connected_peers() - .map(|(peer_id, peer_info)| Peer { - peer_id: peer_id.to_string(), - peer_info: peer_info.clone(), - }) - .collect()) -} - -/// Information returned by `peers` and `connected_peers`. -#[derive(Clone, Debug, Serialize)] -#[serde(bound = "T: EthSpec")] -pub struct Peer<T: EthSpec> { - /// The Peer's ID - peer_id: String, - /// The PeerInfo associated with the peer. - peer_info: PeerInfo<T>, -} diff --git a/beacon_node/rest_api/src/node.rs b/beacon_node/rest_api/src/node.rs deleted file mode 100644 index bd5615de3..000000000 --- a/beacon_node/rest_api/src/node.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{ApiError, Context}; -use beacon_chain::BeaconChainTypes; -use eth2_libp2p::types::SyncState; -use rest_types::{SyncingResponse, SyncingStatus}; -use std::sync::Arc; -use types::Slot; - -/// Returns a syncing status. -pub fn syncing<T: BeaconChainTypes>(ctx: Arc<Context<T>>) -> Result<SyncingResponse, ApiError> { - let current_slot = ctx - .beacon_chain - .head_info() - .map_err(|e| ApiError::ServerError(format!("Unable to read head slot: {:?}", e)))? - .slot; - - let (starting_slot, highest_slot) = match ctx.network_globals.sync_state() { - SyncState::SyncingFinalized { - start_slot, - head_slot, - .. - } - | SyncState::SyncingHead { - start_slot, - head_slot, - } => (start_slot, head_slot), - SyncState::Synced | SyncState::Stalled => (Slot::from(0u64), current_slot), - }; - - let sync_status = SyncingStatus { - starting_slot, - current_slot, - highest_slot, - }; - - Ok(SyncingResponse { - is_syncing: ctx.network_globals.is_syncing(), - sync_status, - }) -} diff --git a/beacon_node/rest_api/src/router.rs b/beacon_node/rest_api/src/router.rs deleted file mode 100644 index bed7ba77a..000000000 --- a/beacon_node/rest_api/src/router.rs +++ /dev/null @@ -1,322 +0,0 @@ -use crate::{ - beacon, config::Config, consensus, lighthouse, metrics, node, validator, NetworkChannel, -}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use bus::Bus; -use environment::TaskExecutor; -use eth2_config::Eth2Config; -use eth2_libp2p::{NetworkGlobals, PeerId}; -use hyper::header::HeaderValue; -use hyper::{Body, Method, Request, Response}; -use lighthouse_version::version_with_platform; -use operation_pool::PersistedOperationPool; -use parking_lot::Mutex; -use rest_types::{ApiError, Handler, Health}; -use slog::debug; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Instant; -use types::{EthSpec, SignedBeaconBlockHash}; - -pub struct Context<T: BeaconChainTypes> { - pub executor: TaskExecutor, - pub config: Config, - pub beacon_chain: Arc<BeaconChain<T>>, - pub network_globals: Arc<NetworkGlobals<T::EthSpec>>, - pub network_chan: NetworkChannel<T::EthSpec>, - pub eth2_config: Arc<Eth2Config>, - pub log: slog::Logger, - pub db_path: PathBuf, - pub freezer_db_path: PathBuf, - pub events: Arc<Mutex<Bus<SignedBeaconBlockHash>>>, -} - -pub async fn on_http_request<T: BeaconChainTypes>( - req: Request<Body>, - ctx: Arc<Context<T>>, -) -> Result<Response<Body>, ApiError> { - let path = req.uri().path().to_string(); - - let _timer = metrics::start_timer_vec(&metrics::BEACON_HTTP_API_TIMES_TOTAL, &[&path]); - metrics::inc_counter_vec(&metrics::BEACON_HTTP_API_REQUESTS_TOTAL, &[&path]); - - let received_instant = Instant::now(); - let log = ctx.log.clone(); - let allow_origin = ctx.config.allow_origin.clone(); - - match route(req, ctx).await { - Ok(mut response) => { - metrics::inc_counter_vec(&metrics::BEACON_HTTP_API_SUCCESS_TOTAL, &[&path]); - - if allow_origin != "" { - let headers = response.headers_mut(); - headers.insert( - hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN, - HeaderValue::from_str(&allow_origin)?, - ); - headers.insert(hyper::header::VARY, HeaderValue::from_static("Origin")); - } - - debug!( - log, - "HTTP API request successful"; - "path" => path, - "duration_ms" => Instant::now().duration_since(received_instant).as_millis() - ); - Ok(response) - } - - Err(error) => { - metrics::inc_counter_vec(&metrics::BEACON_HTTP_API_ERROR_TOTAL, &[&path]); - - debug!( - log, - "HTTP API request failure"; - "path" => path, - "duration_ms" => Instant::now().duration_since(received_instant).as_millis() - ); - Ok(error.into()) - } - } -} - -async fn route<T: BeaconChainTypes>( - req: Request<Body>, - ctx: Arc<Context<T>>, -) -> Result<Response<Body>, ApiError> { - let path = req.uri().path().to_string(); - let ctx = ctx.clone(); - let method = req.method().clone(); - let executor = ctx.executor.clone(); - let handler = Handler::new(req, ctx, executor)?; - - match (method, path.as_ref()) { - (Method::GET, "/node/version") => handler - .static_value(version_with_platform()) - .await? - .serde_encodings(), - (Method::GET, "/node/health") => handler - .static_value(Health::observe().map_err(ApiError::ServerError)?) - .await? - .serde_encodings(), - (Method::GET, "/node/syncing") => handler - .allow_body() - .in_blocking_task(|_, ctx| node::syncing(ctx)) - .await? - .serde_encodings(), - (Method::GET, "/network/enr") => handler - .in_core_task(|_, ctx| Ok(ctx.network_globals.local_enr().to_base64())) - .await? - .serde_encodings(), - (Method::GET, "/network/peer_count") => handler - .in_core_task(|_, ctx| Ok(ctx.network_globals.connected_peers())) - .await? - .serde_encodings(), - (Method::GET, "/network/peer_id") => handler - .in_core_task(|_, ctx| Ok(ctx.network_globals.local_peer_id().to_base58())) - .await? - .serde_encodings(), - (Method::GET, "/network/peers") => handler - .in_blocking_task(|_, ctx| { - Ok(ctx - .network_globals - .peers - .read() - .connected_peer_ids() - .map(PeerId::to_string) - .collect::<Vec<_>>()) - }) - .await? - .serde_encodings(), - (Method::GET, "/network/listen_port") => handler - .in_core_task(|_, ctx| Ok(ctx.network_globals.listen_port_tcp())) - .await? - .serde_encodings(), - (Method::GET, "/network/listen_addresses") => handler - .in_blocking_task(|_, ctx| Ok(ctx.network_globals.listen_multiaddrs())) - .await? - .serde_encodings(), - (Method::GET, "/beacon/head") => handler - .in_blocking_task(|_, ctx| beacon::get_head(ctx)) - .await? - .all_encodings(), - (Method::GET, "/beacon/heads") => handler - .in_blocking_task(|_, ctx| Ok(beacon::get_heads(ctx))) - .await? - .all_encodings(), - (Method::GET, "/beacon/block") => handler - .in_blocking_task(beacon::get_block) - .await? - .all_encodings(), - (Method::GET, "/beacon/block_root") => handler - .in_blocking_task(beacon::get_block_root) - .await? - .all_encodings(), - (Method::GET, "/beacon/fork") => handler - .in_blocking_task(|_, ctx| Ok(ctx.beacon_chain.head_info()?.fork)) - .await? - .all_encodings(), - (Method::GET, "/beacon/fork/stream") => { - handler.sse_stream(|_, ctx| beacon::stream_forks(ctx)).await - } - (Method::GET, "/beacon/genesis_time") => handler - .in_blocking_task(|_, ctx| Ok(ctx.beacon_chain.head_info()?.genesis_time)) - .await? - .all_encodings(), - (Method::GET, "/beacon/genesis_validators_root") => handler - .in_blocking_task(|_, ctx| Ok(ctx.beacon_chain.head_info()?.genesis_validators_root)) - .await? - .all_encodings(), - (Method::GET, "/beacon/validators") => handler - .in_blocking_task(beacon::get_validators) - .await? - .all_encodings(), - (Method::POST, "/beacon/validators") => handler - .allow_body() - .in_blocking_task(beacon::post_validators) - .await? - .all_encodings(), - (Method::GET, "/beacon/validators/all") => handler - .in_blocking_task(beacon::get_all_validators) - .await? - .all_encodings(), - (Method::GET, "/beacon/validators/active") => handler - .in_blocking_task(beacon::get_active_validators) - .await? - .all_encodings(), - (Method::GET, "/beacon/state") => handler - .in_blocking_task(beacon::get_state) - .await? - .all_encodings(), - (Method::GET, "/beacon/state_root") => handler - .in_blocking_task(beacon::get_state_root) - .await? - .all_encodings(), - (Method::GET, "/beacon/state/genesis") => handler - .in_blocking_task(|_, ctx| beacon::get_genesis_state(ctx)) - .await? - .all_encodings(), - (Method::GET, "/beacon/committees") => handler - .in_blocking_task(beacon::get_committees) - .await? - .all_encodings(), - (Method::POST, "/beacon/proposer_slashing") => handler - .allow_body() - .in_blocking_task(beacon::proposer_slashing) - .await? - .serde_encodings(), - (Method::POST, "/beacon/attester_slashing") => handler - .allow_body() - .in_blocking_task(beacon::attester_slashing) - .await? - .serde_encodings(), - (Method::POST, "/validator/duties") => handler - .allow_body() - .in_blocking_task(validator::post_validator_duties) - .await? - .serde_encodings(), - (Method::POST, "/validator/subscribe") => handler - .allow_body() - .in_blocking_task(validator::post_validator_subscriptions) - .await? - .serde_encodings(), - (Method::GET, "/validator/duties/all") => handler - .in_blocking_task(validator::get_all_validator_duties) - .await? - .serde_encodings(), - (Method::GET, "/validator/duties/active") => handler - .in_blocking_task(validator::get_active_validator_duties) - .await? - .serde_encodings(), - (Method::GET, "/validator/block") => handler - .in_blocking_task(validator::get_new_beacon_block) - .await? - .serde_encodings(), - (Method::POST, "/validator/block") => handler - .allow_body() - .in_blocking_task(validator::publish_beacon_block) - .await? - .serde_encodings(), - (Method::GET, "/validator/attestation") => handler - .in_blocking_task(validator::get_new_attestation) - .await? - .serde_encodings(), - (Method::GET, "/validator/aggregate_attestation") => handler - .in_blocking_task(validator::get_aggregate_attestation) - .await? - .serde_encodings(), - (Method::POST, "/validator/attestations") => handler - .allow_body() - .in_blocking_task(validator::publish_attestations) - .await? - .serde_encodings(), - (Method::POST, "/validator/aggregate_and_proofs") => handler - .allow_body() - .in_blocking_task(validator::publish_aggregate_and_proofs) - .await? - .serde_encodings(), - (Method::GET, "/consensus/global_votes") => handler - .allow_body() - .in_blocking_task(consensus::get_vote_count) - .await? - .serde_encodings(), - (Method::POST, "/consensus/individual_votes") => handler - .allow_body() - .in_blocking_task(consensus::post_individual_votes) - .await? - .serde_encodings(), - (Method::GET, "/spec") => handler - // TODO: this clone is not ideal. - .in_blocking_task(|_, ctx| Ok(ctx.beacon_chain.spec.clone())) - .await? - .serde_encodings(), - (Method::GET, "/spec/slots_per_epoch") => handler - .static_value(T::EthSpec::slots_per_epoch()) - .await? - .serde_encodings(), - (Method::GET, "/spec/eth2_config") => handler - // TODO: this clone is not ideal. - .in_blocking_task(|_, ctx| Ok(ctx.eth2_config.as_ref().clone())) - .await? - .serde_encodings(), - (Method::GET, "/advanced/fork_choice") => handler - .in_blocking_task(|_, ctx| { - Ok(ctx - .beacon_chain - .fork_choice - .read() - .proto_array() - .core_proto_array() - .clone()) - }) - .await? - .serde_encodings(), - (Method::GET, "/advanced/operation_pool") => handler - .in_blocking_task(|_, ctx| { - Ok(PersistedOperationPool::from_operation_pool( - &ctx.beacon_chain.op_pool, - )) - }) - .await? - .serde_encodings(), - (Method::GET, "/metrics") => handler - .in_blocking_task(|_, ctx| metrics::get_prometheus(ctx)) - .await? - .text_encoding(), - (Method::GET, "/lighthouse/syncing") => handler - .in_blocking_task(|_, ctx| Ok(ctx.network_globals.sync_state())) - .await? - .serde_encodings(), - (Method::GET, "/lighthouse/peers") => handler - .in_blocking_task(|_, ctx| lighthouse::peers(ctx)) - .await? - .serde_encodings(), - (Method::GET, "/lighthouse/connected_peers") => handler - .in_blocking_task(|_, ctx| lighthouse::connected_peers(ctx)) - .await? - .serde_encodings(), - _ => Err(ApiError::NotFound( - "Request path and/or method not found.".to_owned(), - )), - } -} diff --git a/beacon_node/rest_api/src/url_query.rs b/beacon_node/rest_api/src/url_query.rs deleted file mode 100644 index fee0cf437..000000000 --- a/beacon_node/rest_api/src/url_query.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::helpers::{parse_committee_index, parse_epoch, parse_hex_ssz_bytes, parse_slot}; -use crate::ApiError; -use hyper::Request; -use types::{AttestationData, CommitteeIndex, Epoch, Signature, Slot}; - -/// Provides handy functions for parsing the query parameters of a URL. - -#[derive(Clone, Copy)] -pub struct UrlQuery<'a>(url::form_urlencoded::Parse<'a>); - -impl<'a> UrlQuery<'a> { - /// Instantiate from an existing `Request`. - /// - /// Returns `Err` if `req` does not contain any query parameters. - pub fn from_request<T>(req: &'a Request<T>) -> Result<Self, ApiError> { - let query_str = req.uri().query().unwrap_or_else(|| ""); - - Ok(UrlQuery(url::form_urlencoded::parse(query_str.as_bytes()))) - } - - /// Returns the first `(key, value)` pair found where the `key` is in `keys`. - /// - /// If no match is found, an `InvalidQueryParams` error is returned. - pub fn first_of(mut self, keys: &[&str]) -> Result<(String, String), ApiError> { - self.0 - .find(|(key, _value)| keys.contains(&&**key)) - .map(|(key, value)| (key.into_owned(), value.into_owned())) - .ok_or_else(|| { - ApiError::BadRequest(format!( - "URL query must be valid and contain at least one of the following keys: {:?}", - keys - )) - }) - } - - /// Returns the first `(key, value)` pair found where the `key` is in `keys`, if any. - /// - /// Returns `None` if no match is found. - pub fn first_of_opt(mut self, keys: &[&str]) -> Option<(String, String)> { - self.0 - .find(|(key, _value)| keys.contains(&&**key)) - .map(|(key, value)| (key.into_owned(), value.into_owned())) - } - - /// Returns the value for `key`, if and only if `key` is the only key present in the query - /// parameters. - pub fn only_one(self, key: &str) -> Result<String, ApiError> { - let queries: Vec<_> = self - .0 - .map(|(k, v)| (k.into_owned(), v.into_owned())) - .collect(); - - if queries.len() == 1 { - let (first_key, first_value) = &queries[0]; // Must have 0 index if len is 1. - if first_key == key { - Ok(first_value.to_string()) - } else { - Err(ApiError::BadRequest(format!( - "Only the {} query parameter is supported", - key - ))) - } - } else { - Err(ApiError::BadRequest(format!( - "Only one query parameter is allowed, {} supplied", - queries.len() - ))) - } - } - - /// Returns a vector of all values present where `key` is in `keys - /// - /// If no match is found, an `InvalidQueryParams` error is returned. - pub fn all_of(self, key: &str) -> Result<Vec<String>, ApiError> { - let queries: Vec<_> = self - .0 - .filter_map(|(k, v)| { - if k.eq(key) { - Some(v.into_owned()) - } else { - None - } - }) - .collect(); - Ok(queries) - } - - /// Returns the value of the first occurrence of the `epoch` key. - pub fn epoch(self) -> Result<Epoch, ApiError> { - self.first_of(&["epoch"]) - .and_then(|(_key, value)| parse_epoch(&value)) - } - - /// Returns the value of the first occurrence of the `slot` key. - pub fn slot(self) -> Result<Slot, ApiError> { - self.first_of(&["slot"]) - .and_then(|(_key, value)| parse_slot(&value)) - } - - /// Returns the value of the first occurrence of the `committee_index` key. - pub fn committee_index(self) -> Result<CommitteeIndex, ApiError> { - self.first_of(&["committee_index"]) - .and_then(|(_key, value)| parse_committee_index(&value)) - } - - /// Returns the value of the first occurrence of the `randao_reveal` key. - pub fn randao_reveal(self) -> Result<Signature, ApiError> { - self.first_of(&["randao_reveal"]) - .and_then(|(_key, value)| parse_hex_ssz_bytes(&value)) - } - - /// Returns the value of the first occurrence of the `attestation_data` key. - pub fn attestation_data(self) -> Result<AttestationData, ApiError> { - self.first_of(&["attestation_data"]) - .and_then(|(_key, value)| parse_hex_ssz_bytes(&value)) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn only_one() { - let get_result = |addr: &str, key: &str| -> Result<String, ApiError> { - UrlQuery(url::Url::parse(addr).unwrap().query_pairs()).only_one(key) - }; - - assert_eq!(get_result("http://cat.io/?a=42", "a"), Ok("42".to_string())); - assert!(get_result("http://cat.io/?a=42", "b").is_err()); - assert!(get_result("http://cat.io/?a=42&b=12", "a").is_err()); - assert!(get_result("http://cat.io/", "").is_err()); - } - - #[test] - fn first_of() { - let url = url::Url::parse("http://lighthouse.io/cats?a=42&b=12&c=100").unwrap(); - let get_query = || UrlQuery(url.query_pairs()); - - assert_eq!( - get_query().first_of(&["a"]), - Ok(("a".to_string(), "42".to_string())) - ); - assert_eq!( - get_query().first_of(&["a", "b", "c"]), - Ok(("a".to_string(), "42".to_string())) - ); - assert_eq!( - get_query().first_of(&["a", "a", "a"]), - Ok(("a".to_string(), "42".to_string())) - ); - assert_eq!( - get_query().first_of(&["a", "b", "c"]), - Ok(("a".to_string(), "42".to_string())) - ); - assert_eq!( - get_query().first_of(&["b", "c"]), - Ok(("b".to_string(), "12".to_string())) - ); - assert_eq!( - get_query().first_of(&["c"]), - Ok(("c".to_string(), "100".to_string())) - ); - assert!(get_query().first_of(&["nothing"]).is_err()); - } -} diff --git a/beacon_node/rest_api/src/validator.rs b/beacon_node/rest_api/src/validator.rs deleted file mode 100644 index 49342ddaa..000000000 --- a/beacon_node/rest_api/src/validator.rs +++ /dev/null @@ -1,747 +0,0 @@ -use crate::helpers::{parse_hex_ssz_bytes, publish_beacon_block_to_network}; -use crate::{ApiError, Context, NetworkChannel, UrlQuery}; -use beacon_chain::{ - attestation_verification::Error as AttnError, BeaconChain, BeaconChainError, BeaconChainTypes, - BlockError, ForkChoiceError, StateSkipConfig, -}; -use bls::PublicKeyBytes; -use eth2_libp2p::PubsubMessage; -use hyper::Request; -use network::NetworkMessage; -use rest_types::{ValidatorDutiesRequest, ValidatorDutyBytes, ValidatorSubscription}; -use slog::{error, info, trace, warn, Logger}; -use std::sync::Arc; -use types::beacon_state::EthSpec; -use types::{ - Attestation, AttestationData, BeaconBlock, BeaconState, Epoch, RelativeEpoch, SelectionProof, - SignedAggregateAndProof, SignedBeaconBlock, SubnetId, -}; - -/// HTTP Handler to retrieve the duties for a set of validators during a particular epoch. This -/// method allows for collecting bulk sets of validator duties without risking exceeding the max -/// URL length with query pairs. -pub fn post_validator_duties<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorDutyBytes>, ApiError> { - let body = req.into_body(); - - serde_json::from_slice::<ValidatorDutiesRequest>(&body) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to parse JSON into ValidatorDutiesRequest: {:?}", - e - )) - }) - .and_then(|bulk_request| { - return_validator_duties( - &ctx.beacon_chain.clone(), - bulk_request.epoch, - bulk_request.pubkeys.into_iter().map(Into::into).collect(), - ) - }) -} - -/// HTTP Handler to retrieve subscriptions for a set of validators. This allows the node to -/// organise peer discovery and topic subscription for known validators. -pub fn post_validator_subscriptions<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<(), ApiError> { - let body = req.into_body(); - - serde_json::from_slice(&body) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to parse JSON into ValidatorSubscriptions: {:?}", - e - )) - }) - .and_then(move |subscriptions: Vec<ValidatorSubscription>| { - ctx.network_chan - .send(NetworkMessage::Subscribe { subscriptions }) - .map_err(|e| { - ApiError::ServerError(format!( - "Unable to subscriptions to the network: {:?}", - e - )) - })?; - Ok(()) - }) -} - -/// HTTP Handler to retrieve all validator duties for the given epoch. -pub fn get_all_validator_duties<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorDutyBytes>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let epoch = query.epoch()?; - - let state = get_state_for_epoch(&ctx.beacon_chain, epoch, StateSkipConfig::WithoutStateRoots)?; - - let validator_pubkeys = state - .validators - .iter() - .map(|validator| validator.pubkey.clone()) - .collect(); - - return_validator_duties(&ctx.beacon_chain, epoch, validator_pubkeys) -} - -/// HTTP Handler to retrieve all active validator duties for the given epoch. -pub fn get_active_validator_duties<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Vec<ValidatorDutyBytes>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let epoch = query.epoch()?; - - let state = get_state_for_epoch(&ctx.beacon_chain, epoch, StateSkipConfig::WithoutStateRoots)?; - - let validator_pubkeys = state - .validators - .iter() - .filter(|validator| validator.is_active_at(state.current_epoch())) - .map(|validator| validator.pubkey.clone()) - .collect(); - - return_validator_duties(&ctx.beacon_chain, epoch, validator_pubkeys) -} - -/// Helper function to return the state that can be used to determine the duties for some `epoch`. -pub fn get_state_for_epoch<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - epoch: Epoch, - config: StateSkipConfig, -) -> Result<BeaconState<T::EthSpec>, ApiError> { - let slots_per_epoch = T::EthSpec::slots_per_epoch(); - let head = beacon_chain.head()?; - let current_epoch = beacon_chain.epoch()?; - let head_epoch = head.beacon_state.current_epoch(); - - if head_epoch == current_epoch && RelativeEpoch::from_epoch(current_epoch, epoch).is_ok() { - Ok(head.beacon_state) - } else { - // If epoch is ahead of current epoch, then it should be a "next epoch" request for - // attestation duties. So, go to the start slot of the epoch prior to that, - // which should be just the next wall-clock epoch. - let slot = if epoch > current_epoch { - (epoch - 1).start_slot(slots_per_epoch) - } - // Otherwise, go to the start of the request epoch. - else { - epoch.start_slot(slots_per_epoch) - }; - - beacon_chain.state_at_slot(slot, config).map_err(|e| { - ApiError::ServerError(format!("Unable to load state for epoch {}: {:?}", epoch, e)) - }) - } -} - -/// Helper function to get the duties for some `validator_pubkeys` in some `epoch`. -fn return_validator_duties<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - epoch: Epoch, - validator_pubkeys: Vec<PublicKeyBytes>, -) -> Result<Vec<ValidatorDutyBytes>, ApiError> { - let mut state = get_state_for_epoch(&beacon_chain, epoch, StateSkipConfig::WithoutStateRoots)?; - - let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch) - .map_err(|_| ApiError::ServerError(String::from("Loaded state is in the wrong epoch")))?; - - state - .build_committee_cache(relative_epoch, &beacon_chain.spec) - .map_err(|e| ApiError::ServerError(format!("Unable to build committee cache: {:?}", e)))?; - - // Get a list of all validators for this epoch. - // - // Used for quickly determining the slot for a proposer. - let validator_proposers = if epoch == state.current_epoch() { - Some( - epoch - .slot_iter(T::EthSpec::slots_per_epoch()) - .map(|slot| { - state - .get_beacon_proposer_index(slot, &beacon_chain.spec) - .map(|i| (i, slot)) - .map_err(|e| { - ApiError::ServerError(format!( - "Unable to get proposer index for validator: {:?}", - e - )) - }) - }) - .collect::<Result<Vec<_>, _>>()?, - ) - } else { - None - }; - - validator_pubkeys - .into_iter() - .map(|validator_pubkey| { - // The `beacon_chain` can return a validator index that does not exist in all states. - // Therefore, we must check to ensure that the validator index is valid for our - // `state`. - let validator_index = beacon_chain - .validator_index(&validator_pubkey) - .map_err(|e| { - ApiError::ServerError(format!("Unable to get validator index: {:?}", e)) - })? - .filter(|i| *i < state.validators.len()); - - if let Some(validator_index) = validator_index { - let duties = state - .get_attestation_duties(validator_index, relative_epoch) - .map_err(|e| { - ApiError::ServerError(format!( - "Unable to obtain attestation duties: {:?}", - e - )) - })?; - - let committee_count_at_slot = duties - .map(|d| state.get_committee_count_at_slot(d.slot)) - .transpose() - .map_err(|e| { - ApiError::ServerError(format!( - "Unable to find committee count at slot: {:?}", - e - )) - })?; - - let aggregator_modulo = duties - .map(|duties| SelectionProof::modulo(duties.committee_len, &beacon_chain.spec)) - .transpose() - .map_err(|e| { - ApiError::ServerError(format!("Unable to find modulo: {:?}", e)) - })?; - - let block_proposal_slots = validator_proposers.as_ref().map(|proposers| { - proposers - .iter() - .filter(|(i, _slot)| validator_index == *i) - .map(|(_i, slot)| *slot) - .collect() - }); - - Ok(ValidatorDutyBytes { - validator_pubkey, - validator_index: Some(validator_index as u64), - attestation_slot: duties.map(|d| d.slot), - attestation_committee_index: duties.map(|d| d.index), - committee_count_at_slot, - attestation_committee_position: duties.map(|d| d.committee_position), - block_proposal_slots, - aggregator_modulo, - }) - } else { - Ok(ValidatorDutyBytes { - validator_pubkey, - validator_index: None, - attestation_slot: None, - attestation_committee_index: None, - attestation_committee_position: None, - block_proposal_slots: None, - committee_count_at_slot: None, - aggregator_modulo: None, - }) - } - }) - .collect::<Result<Vec<_>, ApiError>>() -} - -/// HTTP Handler to produce a new BeaconBlock from the current state, ready to be signed by a validator. -pub fn get_new_beacon_block<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<BeaconBlock<T::EthSpec>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let slot = query.slot()?; - let randao_reveal = query.randao_reveal()?; - - let validator_graffiti = if let Some((_key, value)) = query.first_of_opt(&["graffiti"]) { - Some(parse_hex_ssz_bytes(&value)?) - } else { - None - }; - - let (new_block, _state) = ctx - .beacon_chain - .produce_block(randao_reveal, slot, validator_graffiti) - .map_err(|e| { - error!( - ctx.log, - "Error whilst producing block"; - "error" => format!("{:?}", e) - ); - - ApiError::ServerError(format!( - "Beacon node is not able to produce a block: {:?}", - e - )) - })?; - - Ok(new_block) -} - -/// HTTP Handler to publish a SignedBeaconBlock, which has been signed by a validator. -pub fn publish_beacon_block<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<(), ApiError> { - let body = req.into_body(); - - serde_json::from_slice(&body).map_err(|e| { - ApiError::BadRequest(format!("Unable to parse JSON into SignedBeaconBlock: {:?}", e)) - }) - .and_then(move |block: SignedBeaconBlock<T::EthSpec>| { - let slot = block.slot(); - match ctx.beacon_chain.process_block(block.clone()) { - Ok(block_root) => { - // Block was processed, publish via gossipsub - info!( - ctx.log, - "Block from local validator"; - "block_root" => format!("{}", block_root), - "block_slot" => slot, - ); - - publish_beacon_block_to_network::<T>(&ctx.network_chan, block)?; - - // Run the fork choice algorithm and enshrine a new canonical head, if - // found. - // - // The new head may or may not be the block we just received. - if let Err(e) = ctx.beacon_chain.fork_choice() { - error!( - ctx.log, - "Failed to find beacon chain head"; - "error" => format!("{:?}", e) - ); - } else { - // In the best case, validators should produce blocks that become the - // head. - // - // Potential reasons this may not be the case: - // - // - A quick re-org between block produce and publish. - // - Excessive time between block produce and publish. - // - A validator is using another beacon node to produce blocks and - // submitting them here. - if ctx.beacon_chain.head()?.beacon_block_root != block_root { - warn!( - ctx.log, - "Block from validator is not head"; - "desc" => "potential re-org", - ); - - } - } - - Ok(()) - } - Err(BlockError::BeaconChainError(e)) => { - error!( - ctx.log, - "Error whilst processing block"; - "error" => format!("{:?}", e) - ); - - Err(ApiError::ServerError(format!( - "Error while processing block: {:?}", - e - ))) - } - Err(other) => { - warn!( - ctx.log, - "Invalid block from local validator"; - "outcome" => format!("{:?}", other) - ); - - Err(ApiError::ProcessingError(format!( - "The SignedBeaconBlock could not be processed and has not been published: {:?}", - other - ))) - } - } - }) -} - -/// HTTP Handler to produce a new Attestation from the current state, ready to be signed by a validator. -pub fn get_new_attestation<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Attestation<T::EthSpec>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let slot = query.slot()?; - let index = query.committee_index()?; - - ctx.beacon_chain - .produce_unaggregated_attestation(slot, index) - .map_err(|e| ApiError::BadRequest(format!("Unable to produce attestation: {:?}", e))) -} - -/// HTTP Handler to retrieve the aggregate attestation for a slot -pub fn get_aggregate_attestation<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<Attestation<T::EthSpec>, ApiError> { - let query = UrlQuery::from_request(&req)?; - - let attestation_data = query.attestation_data()?; - - match ctx - .beacon_chain - .get_aggregated_attestation(&attestation_data) - { - Ok(Some(attestation)) => Ok(attestation), - Ok(None) => Err(ApiError::NotFound(format!( - "No matching aggregate attestation for slot {:?} is known in slot {:?}", - attestation_data.slot, - ctx.beacon_chain.slot() - ))), - Err(e) => Err(ApiError::ServerError(format!( - "Unable to obtain attestation: {:?}", - e - ))), - } -} - -/// HTTP Handler to publish a list of Attestations, which have been signed by a number of validators. -pub fn publish_attestations<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<(), ApiError> { - let bytes = req.into_body(); - - serde_json::from_slice(&bytes) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to deserialize JSON into a list of attestations: {:?}", - e - )) - }) - // Process all of the aggregates _without_ exiting early if one fails. - .map( - move |attestations: Vec<(Attestation<T::EthSpec>, SubnetId)>| { - attestations - .into_iter() - .enumerate() - .map(|(i, (attestation, subnet_id))| { - process_unaggregated_attestation( - &ctx.beacon_chain, - ctx.network_chan.clone(), - attestation, - subnet_id, - i, - &ctx.log, - ) - }) - .collect::<Vec<Result<_, _>>>() - }, - ) - // Iterate through all the results and return on the first `Err`. - // - // Note: this will only provide info about the _first_ failure, not all failures. - .and_then(|processing_results| processing_results.into_iter().try_for_each(|result| result)) - .map(|_| ()) -} - -/// Processes an unaggregrated attestation that was included in a list of attestations with the -/// index `i`. -#[allow(clippy::redundant_clone)] // false positives in this function. -fn process_unaggregated_attestation<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - network_chan: NetworkChannel<T::EthSpec>, - attestation: Attestation<T::EthSpec>, - subnet_id: SubnetId, - i: usize, - log: &Logger, -) -> Result<(), ApiError> { - let data = &attestation.data.clone(); - - // Verify that the attestation is valid to included on the gossip network. - let verified_attestation = beacon_chain - .verify_unaggregated_attestation_for_gossip(attestation.clone(), subnet_id) - .map_err(|e| { - handle_attestation_error( - e, - &format!("unaggregated attestation {} failed gossip verification", i), - data, - log, - ) - })?; - - // Publish the attestation to the network - if let Err(e) = network_chan.send(NetworkMessage::Publish { - messages: vec![PubsubMessage::Attestation(Box::new(( - subnet_id, - attestation, - )))], - }) { - return Err(ApiError::ServerError(format!( - "Unable to send unaggregated attestation {} to network: {:?}", - i, e - ))); - } - - beacon_chain - .apply_attestation_to_fork_choice(&verified_attestation) - .map_err(|e| { - handle_fork_choice_error( - e, - &format!( - "unaggregated attestation {} was unable to be added to fork choice", - i - ), - data, - log, - ) - })?; - - beacon_chain - .add_to_naive_aggregation_pool(verified_attestation) - .map_err(|e| { - handle_attestation_error( - e, - &format!( - "unaggregated attestation {} was unable to be added to aggregation pool", - i - ), - data, - log, - ) - })?; - - Ok(()) -} - -/// HTTP Handler to publish an Attestation, which has been signed by a validator. -pub fn publish_aggregate_and_proofs<T: BeaconChainTypes>( - req: Request<Vec<u8>>, - ctx: Arc<Context<T>>, -) -> Result<(), ApiError> { - let body = req.into_body(); - - serde_json::from_slice(&body) - .map_err(|e| { - ApiError::BadRequest(format!( - "Unable to deserialize JSON into a list of SignedAggregateAndProof: {:?}", - e - )) - }) - // Process all of the aggregates _without_ exiting early if one fails. - .map( - move |signed_aggregates: Vec<SignedAggregateAndProof<T::EthSpec>>| { - signed_aggregates - .into_iter() - .enumerate() - .map(|(i, signed_aggregate)| { - process_aggregated_attestation( - &ctx.beacon_chain, - ctx.network_chan.clone(), - signed_aggregate, - i, - &ctx.log, - ) - }) - .collect::<Vec<Result<_, _>>>() - }, - ) - // Iterate through all the results and return on the first `Err`. - // - // Note: this will only provide info about the _first_ failure, not all failures. - .and_then(|processing_results| processing_results.into_iter().try_for_each(|result| result)) -} - -/// Processes an aggregrated attestation that was included in a list of attestations with the index -/// `i`. -#[allow(clippy::redundant_clone)] // false positives in this function. -fn process_aggregated_attestation<T: BeaconChainTypes>( - beacon_chain: &BeaconChain<T>, - network_chan: NetworkChannel<T::EthSpec>, - signed_aggregate: SignedAggregateAndProof<T::EthSpec>, - i: usize, - log: &Logger, -) -> Result<(), ApiError> { - let data = &signed_aggregate.message.aggregate.data.clone(); - - // Verify that the attestation is valid to be included on the gossip network. - // - // Using this gossip check for local validators is not necessarily ideal, there will be some - // attestations that we reject that could possibly be included in a block (e.g., attestations - // that late by more than 1 epoch but less than 2). We can come pick this back up if we notice - // that it's materially affecting validator profits. Until then, I'm hesitant to introduce yet - // _another_ attestation verification path. - let verified_attestation = - match beacon_chain.verify_aggregated_attestation_for_gossip(signed_aggregate.clone()) { - Ok(verified_attestation) => verified_attestation, - Err(AttnError::AttestationAlreadyKnown(attestation_root)) => { - trace!( - log, - "Ignored known attn from local validator"; - "attn_root" => format!("{}", attestation_root) - ); - - // Exit early with success for a known attestation, there's no need to re-process - // an aggregate we already know. - return Ok(()); - } - /* - * It's worth noting that we don't check for `Error::AggregatorAlreadyKnown` since (at - * the time of writing) we check for `AttestationAlreadyKnown` first. - * - * Given this, it's impossible to hit `Error::AggregatorAlreadyKnown` without that - * aggregator having already produced a conflicting aggregation. This is not slashable - * but I think it's still the sort of condition we should error on, at least for now. - */ - Err(e) => { - return Err(handle_attestation_error( - e, - &format!("aggregated attestation {} failed gossip verification", i), - data, - log, - )) - } - }; - - // Publish the attestation to the network - if let Err(e) = network_chan.send(NetworkMessage::Publish { - messages: vec![PubsubMessage::AggregateAndProofAttestation(Box::new( - signed_aggregate, - ))], - }) { - return Err(ApiError::ServerError(format!( - "Unable to send aggregated attestation {} to network: {:?}", - i, e - ))); - } - - beacon_chain - .apply_attestation_to_fork_choice(&verified_attestation) - .map_err(|e| { - handle_fork_choice_error( - e, - &format!( - "aggregated attestation {} was unable to be added to fork choice", - i - ), - data, - log, - ) - })?; - - beacon_chain - .add_to_block_inclusion_pool(verified_attestation) - .map_err(|e| { - handle_attestation_error( - e, - &format!( - "aggregated attestation {} was unable to be added to op pool", - i - ), - data, - log, - ) - })?; - - Ok(()) -} - -/// Common handler for `AttnError` during attestation verification. -fn handle_attestation_error( - e: AttnError, - detail: &str, - data: &AttestationData, - log: &Logger, -) -> ApiError { - match e { - AttnError::BeaconChainError(e) => { - error!( - log, - "Internal error verifying local attestation"; - "detail" => detail, - "error" => format!("{:?}", e), - "target" => data.target.epoch, - "source" => data.source.epoch, - "index" => data.index, - "slot" => data.slot, - ); - - ApiError::ServerError(format!( - "Internal error verifying local attestation. Error: {:?}. Detail: {}", - e, detail - )) - } - e => { - error!( - log, - "Invalid local attestation"; - "detail" => detail, - "reason" => format!("{:?}", e), - "target" => data.target.epoch, - "source" => data.source.epoch, - "index" => data.index, - "slot" => data.slot, - ); - - ApiError::ProcessingError(format!( - "Invalid local attestation. Error: {:?} Detail: {}", - e, detail - )) - } - } -} - -/// Common handler for `ForkChoiceError` during attestation verification. -fn handle_fork_choice_error( - e: BeaconChainError, - detail: &str, - data: &AttestationData, - log: &Logger, -) -> ApiError { - match e { - BeaconChainError::ForkChoiceError(ForkChoiceError::InvalidAttestation(e)) => { - error!( - log, - "Local attestation invalid for fork choice"; - "detail" => detail, - "reason" => format!("{:?}", e), - "target" => data.target.epoch, - "source" => data.source.epoch, - "index" => data.index, - "slot" => data.slot, - ); - - ApiError::ProcessingError(format!( - "Invalid local attestation. Error: {:?} Detail: {}", - e, detail - )) - } - e => { - error!( - log, - "Internal error applying attn to fork choice"; - "detail" => detail, - "error" => format!("{:?}", e), - "target" => data.target.epoch, - "source" => data.source.epoch, - "index" => data.index, - "slot" => data.slot, - ); - - ApiError::ServerError(format!( - "Internal error verifying local attestation. Error: {:?}. Detail: {}", - e, detail - )) - } - } -} diff --git a/beacon_node/rest_api/tests/test.rs b/beacon_node/rest_api/tests/test.rs deleted file mode 100644 index 160ee667c..000000000 --- a/beacon_node/rest_api/tests/test.rs +++ /dev/null @@ -1,1345 +0,0 @@ -#![cfg(test)] - -#[macro_use] -extern crate assert_matches; - -use beacon_chain::{BeaconChain, BeaconChainTypes, StateSkipConfig}; -use node_test_rig::{ - environment::{Environment, EnvironmentBuilder}, - testing_client_config, ClientConfig, ClientGenesis, LocalBeaconNode, -}; -use remote_beacon_node::{ - Committee, HeadBeaconBlock, PersistedOperationPool, PublishStatus, ValidatorResponse, -}; -use rest_types::ValidatorDutyBytes; -use std::convert::TryInto; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use types::{ - test_utils::{ - build_double_vote_attester_slashing, build_proposer_slashing, - generate_deterministic_keypair, AttesterSlashingTestTask, ProposerSlashingTestTask, - }, - BeaconBlock, BeaconState, ChainSpec, Domain, Epoch, EthSpec, MinimalEthSpec, PublicKey, - RelativeEpoch, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedRoot, Slot, - SubnetId, Validator, -}; - -type E = MinimalEthSpec; - -fn build_env() -> Environment<E> { - EnvironmentBuilder::minimal() - .null_logger() - .expect("should build env logger") - .single_thread_tokio_runtime() - .expect("should start tokio runtime") - .build() - .expect("environment should build") -} - -fn build_node<E: EthSpec>(env: &mut Environment<E>, config: ClientConfig) -> LocalBeaconNode<E> { - let context = env.core_context(); - env.runtime() - .block_on(LocalBeaconNode::production(context, config)) - .expect("should block until node created") -} - -/// Returns the randao reveal for the given slot (assuming the given `beacon_chain` uses -/// deterministic keypairs). -fn get_randao_reveal<T: BeaconChainTypes>( - beacon_chain: Arc<BeaconChain<T>>, - slot: Slot, - spec: &ChainSpec, -) -> Signature { - let head = beacon_chain.head().expect("should get head"); - let fork = head.beacon_state.fork; - let genesis_validators_root = head.beacon_state.genesis_validators_root; - let proposer_index = beacon_chain - .block_proposer(slot) - .expect("should get proposer index"); - let keypair = generate_deterministic_keypair(proposer_index); - let epoch = slot.epoch(E::slots_per_epoch()); - let domain = spec.get_domain(epoch, Domain::Randao, &fork, genesis_validators_root); - let message = epoch.signing_root(domain); - keypair.sk.sign(message) -} - -/// Signs the given block (assuming the given `beacon_chain` uses deterministic keypairs). -fn sign_block<T: BeaconChainTypes>( - beacon_chain: Arc<BeaconChain<T>>, - block: BeaconBlock<T::EthSpec>, - spec: &ChainSpec, -) -> SignedBeaconBlock<T::EthSpec> { - let head = beacon_chain.head().expect("should get head"); - let fork = head.beacon_state.fork; - let genesis_validators_root = head.beacon_state.genesis_validators_root; - let proposer_index = beacon_chain - .block_proposer(block.slot) - .expect("should get proposer index"); - let keypair = generate_deterministic_keypair(proposer_index); - block.sign(&keypair.sk, &fork, genesis_validators_root, spec) -} - -#[test] -fn validator_produce_attestation() { - let mut env = build_env(); - - let spec = &E::default_spec(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - let genesis_validators_root = beacon_chain.genesis_validators_root; - let state = beacon_chain.head().expect("should get head").beacon_state; - - // Find a validator that has duties in the current slot of the chain. - let mut validator_index = 0; - let duties = loop { - let duties = state - .get_attestation_duties(validator_index, RelativeEpoch::Current) - .expect("should have attestation duties cache") - .expect("should have attestation duties"); - - if duties.slot == node.client.beacon_chain().unwrap().slot().unwrap() { - break duties; - } else { - validator_index += 1 - } - }; - - let mut attestation = env - .runtime() - .block_on( - remote_node - .http - .validator() - .produce_attestation(duties.slot, duties.index), - ) - .expect("should fetch attestation from http api"); - - assert_eq!( - attestation.data.index, duties.index, - "should have same index" - ); - assert_eq!(attestation.data.slot, duties.slot, "should have same slot"); - assert_eq!( - attestation.aggregation_bits.num_set_bits(), - 0, - "should have empty aggregation bits" - ); - - let keypair = generate_deterministic_keypair(validator_index); - - // Fetch the duties again, but via HTTP for authenticity. - let duties = env - .runtime() - .block_on(remote_node.http.validator().get_duties( - attestation.data.slot.epoch(E::slots_per_epoch()), - &[keypair.pk.clone()], - )) - .expect("should fetch duties from http api"); - let duties = &duties[0]; - let committee_count = duties - .committee_count_at_slot - .expect("should have committee count"); - let subnet_id = SubnetId::compute_subnet::<E>( - attestation.data.slot, - attestation.data.index, - committee_count, - spec, - ) - .unwrap(); - // Try publishing the attestation without a signature or a committee bit set, ensure it is - // raises an error. - let publish_status = env - .runtime() - .block_on( - remote_node - .http - .validator() - .publish_attestations(vec![(attestation.clone(), subnet_id)]), - ) - .expect("should publish unsigned attestation"); - assert!( - !publish_status.is_valid(), - "the unsigned published attestation should be invalid" - ); - - // Set the aggregation bit. - attestation - .aggregation_bits - .set( - duties - .attestation_committee_position - .expect("should have committee position"), - true, - ) - .expect("should set attestation bit"); - - // Try publishing with an aggreagation bit set, but an invalid signature. - let publish_status = env - .runtime() - .block_on( - remote_node - .http - .validator() - .publish_attestations(vec![(attestation.clone(), subnet_id)]), - ) - .expect("should publish attestation with invalid signature"); - assert!( - !publish_status.is_valid(), - "the unsigned published attestation should not be valid" - ); - - // Un-set the aggregation bit, so signing doesn't error. - attestation - .aggregation_bits - .set( - duties - .attestation_committee_position - .expect("should have committee position"), - false, - ) - .expect("should un-set attestation bit"); - - attestation - .sign( - &keypair.sk, - duties - .attestation_committee_position - .expect("should have committee position"), - &state.fork, - state.genesis_validators_root, - spec, - ) - .expect("should sign attestation"); - - // Try publishing the valid attestation. - let publish_status = env - .runtime() - .block_on( - remote_node - .http - .validator() - .publish_attestations(vec![(attestation.clone(), subnet_id)]), - ) - .expect("should publish attestation"); - assert!( - publish_status.is_valid(), - "the signed published attestation should be valid" - ); - - // Try obtaining an aggregated attestation with a matching attestation data to the previous - // one. - let aggregated_attestation = env - .runtime() - .block_on( - remote_node - .http - .validator() - .produce_aggregate_attestation(&attestation.data), - ) - .expect("should fetch aggregated attestation from http api"); - - let signed_aggregate_and_proof = SignedAggregateAndProof::from_aggregate( - validator_index as u64, - aggregated_attestation, - None, - &keypair.sk, - &state.fork, - genesis_validators_root, - spec, - ); - - // Publish the signed aggregate. - let publish_status = env - .runtime() - .block_on( - remote_node - .http - .validator() - .publish_aggregate_and_proof(vec![signed_aggregate_and_proof]), - ) - .expect("should publish aggregate and proof"); - assert!( - publish_status.is_valid(), - "the signed aggregate and proof should be valid" - ); -} - -#[test] -fn validator_duties() { - let mut env = build_env(); - - let spec = &E::default_spec(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - - let mut epoch = Epoch::new(0); - - let validators = beacon_chain - .head() - .expect("should get head") - .beacon_state - .validators - .iter() - .map(|v| (&v.pubkey).try_into().expect("pubkey should be valid")) - .collect::<Vec<_>>(); - - let duties = env - .runtime() - .block_on(remote_node.http.validator().get_duties(epoch, &validators)) - .expect("should fetch duties from http api"); - - // 1. Check at the current epoch. - check_duties( - duties, - epoch, - validators.clone(), - beacon_chain.clone(), - spec, - ); - - epoch += 4; - let duties = env - .runtime() - .block_on(remote_node.http.validator().get_duties(epoch, &validators)) - .expect("should fetch duties from http api"); - - // 2. Check with a long skip forward. - check_duties(duties, epoch, validators, beacon_chain, spec); - - // TODO: test an epoch in the past. Blocked because the `LocalBeaconNode` cannot produce a - // chain, yet. -} - -fn check_duties<T: BeaconChainTypes>( - duties: Vec<ValidatorDutyBytes>, - epoch: Epoch, - validators: Vec<PublicKey>, - beacon_chain: Arc<BeaconChain<T>>, - spec: &ChainSpec, -) { - assert_eq!( - validators.len(), - duties.len(), - "there should be a duty for each validator" - ); - - // Are the duties from the current epoch of the beacon chain, and thus are proposer indices - // known? - let proposers_known = epoch == beacon_chain.epoch().unwrap(); - - let mut state = beacon_chain - .state_at_slot( - epoch.start_slot(T::EthSpec::slots_per_epoch()), - StateSkipConfig::WithStateRoots, - ) - .expect("should get state at slot"); - - state.build_all_caches(spec).expect("should build caches"); - - validators - .iter() - .zip(duties.iter()) - .for_each(|(validator, duty)| { - assert_eq!( - *validator, - (&duty.validator_pubkey) - .try_into() - .expect("should be valid pubkey"), - "pubkey should match" - ); - - let validator_index = state - .get_validator_index(&validator.clone().into()) - .expect("should have pubkey cache") - .expect("pubkey should exist"); - - let attestation_duty = state - .get_attestation_duties(validator_index, RelativeEpoch::Current) - .expect("should have attestation duties cache") - .expect("should have attestation duties"); - - assert_eq!( - Some(attestation_duty.slot), - duty.attestation_slot, - "attestation slot should match" - ); - - assert_eq!( - Some(attestation_duty.index), - duty.attestation_committee_index, - "attestation index should match" - ); - - if proposers_known { - let block_proposal_slots = duty.block_proposal_slots.as_ref().unwrap(); - - if !block_proposal_slots.is_empty() { - for slot in block_proposal_slots { - let expected_proposer = state - .get_beacon_proposer_index(*slot, spec) - .expect("should know proposer"); - assert_eq!( - expected_proposer, validator_index, - "should get correct proposal slot" - ); - } - } else { - epoch.slot_iter(E::slots_per_epoch()).for_each(|slot| { - let slot_proposer = state - .get_beacon_proposer_index(slot, spec) - .expect("should know proposer"); - assert_ne!( - slot_proposer, validator_index, - "validator should not have proposal slot in this epoch" - ) - }) - } - } else { - assert_eq!(duty.block_proposal_slots, None); - } - }); - - if proposers_known { - // Validator duties should include a proposer for every slot of the epoch. - let mut all_proposer_slots: Vec<Slot> = duties - .iter() - .flat_map(|duty| duty.block_proposal_slots.clone().unwrap()) - .collect(); - all_proposer_slots.sort(); - - let all_slots: Vec<Slot> = epoch.slot_iter(E::slots_per_epoch()).collect(); - assert_eq!(all_proposer_slots, all_slots); - } -} - -#[test] -fn validator_block_post() { - let mut env = build_env(); - - let spec = &E::default_spec(); - - let two_slots_secs = (spec.milliseconds_per_slot / 1_000) * 2; - - let mut config = testing_client_config(); - config.genesis = ClientGenesis::Interop { - validator_count: 8, - genesis_time: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - - two_slots_secs, - }; - - let node = build_node(&mut env, config); - let remote_node = node.remote_node().expect("should produce remote node"); - - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - - let slot = Slot::new(1); - let randao_reveal = get_randao_reveal(beacon_chain.clone(), slot, spec); - - let block = env - .runtime() - .block_on( - remote_node - .http - .validator() - .produce_block(slot, randao_reveal, None), - ) - .expect("should fetch block from http api"); - - // Try publishing the block without a signature, ensure it is flagged as invalid. - let empty_sig_block = SignedBeaconBlock { - message: block.clone(), - signature: Signature::empty(), - }; - let publish_status = env - .runtime() - .block_on(remote_node.http.validator().publish_block(empty_sig_block)) - .expect("should publish block"); - if cfg!(not(feature = "fake_crypto")) { - assert!( - !publish_status.is_valid(), - "the unsigned published block should not be valid" - ); - } - - let signed_block = sign_block(beacon_chain.clone(), block, spec); - let block_root = signed_block.canonical_root(); - - let publish_status = env - .runtime() - .block_on(remote_node.http.validator().publish_block(signed_block)) - .expect("should publish block"); - - if cfg!(not(feature = "fake_crypto")) { - assert_eq!( - publish_status, - PublishStatus::Valid, - "the signed published block should be valid" - ); - } - - let head = env - .runtime() - .block_on(remote_node.http.beacon().get_head()) - .expect("should get head"); - - assert_eq!( - head.block_root, block_root, - "the published block should become the head block" - ); - - // Note: this heads check is not super useful for this test, however it is include so it get - // _some_ testing. If you remove this call, make sure it's tested somewhere else. - let heads = env - .runtime() - .block_on(remote_node.http.beacon().get_heads()) - .expect("should get heads"); - - assert_eq!(heads.len(), 1, "there should be only one head"); - assert_eq!( - heads, - vec![HeadBeaconBlock { - beacon_block_root: head.block_root, - beacon_block_slot: head.slot, - }], - "there should be only one head" - ); -} - -#[test] -fn validator_block_get() { - let mut env = build_env(); - - let spec = &E::default_spec(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - - let slot = Slot::new(1); - let randao_reveal = get_randao_reveal(beacon_chain, slot, spec); - - let block = env - .runtime() - .block_on( - remote_node - .http - .validator() - .produce_block(slot, randao_reveal.clone(), None), - ) - .expect("should fetch block from http api"); - - let (expected_block, _state) = node - .client - .beacon_chain() - .expect("client should have beacon chain") - .produce_block(randao_reveal, slot, None) - .expect("should produce block"); - - assert_eq!( - block, expected_block, - "the block returned from the API should be as expected" - ); -} - -#[test] -fn validator_block_get_with_graffiti() { - let mut env = build_env(); - - let spec = &E::default_spec(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - - let slot = Slot::new(1); - let randao_reveal = get_randao_reveal(beacon_chain, slot, spec); - - let block = env - .runtime() - .block_on(remote_node.http.validator().produce_block( - slot, - randao_reveal.clone(), - Some(*b"test-graffiti-test-graffiti-test"), - )) - .expect("should fetch block from http api"); - - let (expected_block, _state) = node - .client - .beacon_chain() - .expect("client should have beacon chain") - .produce_block( - randao_reveal, - slot, - Some(*b"test-graffiti-test-graffiti-test"), - ) - .expect("should produce block"); - - assert_eq!( - block, expected_block, - "the block returned from the API should be as expected" - ); -} - -#[test] -fn beacon_state() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let (state_by_slot, root) = env - .runtime() - .block_on(remote_node.http.beacon().get_state_by_slot(Slot::new(0))) - .expect("should fetch state from http api"); - - let (state_by_root, root_2) = env - .runtime() - .block_on(remote_node.http.beacon().get_state_by_root(root)) - .expect("should fetch state from http api"); - - let mut db_state = node - .client - .beacon_chain() - .expect("client should have beacon chain") - .state_at_slot(Slot::new(0), StateSkipConfig::WithStateRoots) - .expect("should find state"); - db_state.drop_all_caches(); - - assert_eq!( - root, root_2, - "the two roots returned from the api should be identical" - ); - assert_eq!( - root, - db_state.canonical_root(), - "root from database should match that from the API" - ); - assert_eq!( - state_by_slot, db_state, - "genesis state by slot from api should match that from the DB" - ); - assert_eq!( - state_by_root, db_state, - "genesis state by root from api should match that from the DB" - ); -} - -#[test] -fn beacon_block() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let (block_by_slot, root) = env - .runtime() - .block_on(remote_node.http.beacon().get_block_by_slot(Slot::new(0))) - .expect("should fetch block from http api"); - - let (block_by_root, root_2) = env - .runtime() - .block_on(remote_node.http.beacon().get_block_by_root(root)) - .expect("should fetch block from http api"); - - let db_block = node - .client - .beacon_chain() - .expect("client should have beacon chain") - .block_at_slot(Slot::new(0)) - .expect("should find block") - .expect("block should not be none"); - - assert_eq!( - root, root_2, - "the two roots returned from the api should be identical" - ); - assert_eq!( - root, - db_block.canonical_root(), - "root from database should match that from the API" - ); - assert_eq!( - block_by_slot, db_block, - "genesis block by slot from api should match that from the DB" - ); - assert_eq!( - block_by_root, db_block, - "genesis block by root from api should match that from the DB" - ); -} - -#[test] -fn genesis_time() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let genesis_time = env - .runtime() - .block_on(remote_node.http.beacon().get_genesis_time()) - .expect("should fetch genesis time from http api"); - - assert_eq!( - node.client - .beacon_chain() - .expect("should have beacon chain") - .head() - .expect("should get head") - .beacon_state - .genesis_time, - genesis_time, - "should match genesis time from head state" - ); -} - -#[test] -fn genesis_validators_root() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let genesis_validators_root = env - .runtime() - .block_on(remote_node.http.beacon().get_genesis_validators_root()) - .expect("should fetch genesis time from http api"); - - assert_eq!( - node.client - .beacon_chain() - .expect("should have beacon chain") - .head() - .expect("should get head") - .beacon_state - .genesis_validators_root, - genesis_validators_root, - "should match genesis time from head state" - ); -} - -#[test] -fn fork() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let fork = env - .runtime() - .block_on(remote_node.http.beacon().get_fork()) - .expect("should fetch from http api"); - - assert_eq!( - node.client - .beacon_chain() - .expect("should have beacon chain") - .head() - .expect("should get head") - .beacon_state - .fork, - fork, - "should match head state" - ); -} - -#[test] -fn eth2_config() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let eth2_config = env - .runtime() - .block_on(remote_node.http.spec().get_eth2_config()) - .expect("should fetch eth2 config from http api"); - - // TODO: check the entire eth2_config, not just the spec. - - assert_eq!( - node.client - .beacon_chain() - .expect("should have beacon chain") - .spec, - eth2_config.spec, - "should match genesis time from head state" - ); -} - -#[test] -fn get_version() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let version = env - .runtime() - .block_on(remote_node.http.node().get_version()) - .expect("should fetch version from http api"); - - assert_eq!( - lighthouse_version::version_with_platform(), - version, - "result should be as expected" - ); -} - -#[test] -fn get_genesis_state_root() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let slot = Slot::new(0); - - let result = env - .runtime() - .block_on(remote_node.http.beacon().get_state_root(slot)) - .expect("should fetch from http api"); - - let expected = node - .client - .beacon_chain() - .expect("should have beacon chain") - .rev_iter_state_roots() - .expect("should get iter") - .map(Result::unwrap) - .find(|(_cur_root, cur_slot)| slot == *cur_slot) - .map(|(cur_root, _)| cur_root) - .expect("chain should have state root at slot"); - - assert_eq!(result, expected, "result should be as expected"); -} - -#[test] -fn get_genesis_block_root() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let slot = Slot::new(0); - - let result = env - .runtime() - .block_on(remote_node.http.beacon().get_block_root(slot)) - .expect("should fetch from http api"); - - let expected = node - .client - .beacon_chain() - .expect("should have beacon chain") - .rev_iter_block_roots() - .expect("should get iter") - .map(Result::unwrap) - .find(|(_cur_root, cur_slot)| slot == *cur_slot) - .map(|(cur_root, _)| cur_root) - .expect("chain should have state root at slot"); - - assert_eq!(result, expected, "result should be as expected"); -} - -#[test] -fn get_validators() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let chain = node - .client - .beacon_chain() - .expect("node should have beacon chain"); - let state = &chain.head().expect("should get head").beacon_state; - - let validators = state.validators.iter().take(2).collect::<Vec<_>>(); - let pubkeys = validators - .iter() - .map(|v| (&v.pubkey).try_into().expect("should decode pubkey bytes")) - .collect(); - - let result = env - .runtime() - .block_on(remote_node.http.beacon().get_validators(pubkeys, None)) - .expect("should fetch from http api"); - - result - .iter() - .zip(validators.iter()) - .for_each(|(response, validator)| compare_validator_response(state, response, validator)); -} - -#[test] -fn get_all_validators() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let chain = node - .client - .beacon_chain() - .expect("node should have beacon chain"); - let state = &chain.head().expect("should get head").beacon_state; - - let result = env - .runtime() - .block_on(remote_node.http.beacon().get_all_validators(None)) - .expect("should fetch from http api"); - - result - .iter() - .zip(state.validators.iter()) - .for_each(|(response, validator)| compare_validator_response(state, response, validator)); -} - -#[test] -fn get_active_validators() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let chain = node - .client - .beacon_chain() - .expect("node should have beacon chain"); - let state = &chain.head().expect("should get head").beacon_state; - - let result = env - .runtime() - .block_on(remote_node.http.beacon().get_active_validators(None)) - .expect("should fetch from http api"); - - /* - * This test isn't comprehensive because all of the validators in the state are active (i.e., - * there is no one to exclude. - * - * This should be fixed once we can generate more interesting scenarios with the - * `NodeTestRig`. - */ - - let validators = state - .validators - .iter() - .filter(|validator| validator.is_active_at(state.current_epoch())); - - result - .iter() - .zip(validators) - .for_each(|(response, validator)| compare_validator_response(state, response, validator)); -} - -#[test] -fn get_committees() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let chain = node - .client - .beacon_chain() - .expect("node should have beacon chain"); - - let epoch = Epoch::new(0); - - let result = env - .runtime() - .block_on(remote_node.http.beacon().get_committees(epoch)) - .expect("should fetch from http api"); - - let expected = chain - .head() - .expect("should get head") - .beacon_state - .get_beacon_committees_at_epoch(RelativeEpoch::Current) - .expect("should get committees") - .iter() - .map(|c| Committee { - slot: c.slot, - index: c.index, - committee: c.committee.to_vec(), - }) - .collect::<Vec<_>>(); - - assert_eq!(result, expected, "result should be as expected"); -} - -#[test] -fn get_fork_choice() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let fork_choice = env - .runtime() - .block_on(remote_node.http.advanced().get_fork_choice()) - .expect("should not error when getting fork choice"); - - assert_eq!( - fork_choice, - *node - .client - .beacon_chain() - .expect("node should have beacon chain") - .fork_choice - .read() - .proto_array() - .core_proto_array(), - "result should be as expected" - ); -} - -#[test] -fn get_operation_pool() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let result = env - .runtime() - .block_on(remote_node.http.advanced().get_operation_pool()) - .expect("should not error when getting fork choice"); - - let expected = PersistedOperationPool::from_operation_pool( - &node - .client - .beacon_chain() - .expect("node should have chain") - .op_pool, - ); - - assert_eq!(result, expected, "result should be as expected"); -} - -fn compare_validator_response<T: EthSpec>( - state: &BeaconState<T>, - response: &ValidatorResponse, - validator: &Validator, -) { - let response_validator = response.validator.clone().expect("should have validator"); - let i = response - .validator_index - .expect("should have validator index"); - let balance = response.balance.expect("should have balance"); - - assert_eq!(response.pubkey, validator.pubkey, "pubkey"); - assert_eq!(response_validator, *validator, "validator"); - assert_eq!(state.balances[i], balance, "balances"); - assert_eq!(state.validators[i], *validator, "validator index"); -} - -#[test] -fn proposer_slashing() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let chain = node - .client - .beacon_chain() - .expect("node should have beacon chain"); - - let state = chain - .head() - .expect("should have retrieved state") - .beacon_state; - - let spec = &chain.spec; - - // Check that there are no proposer slashings before insertion - let (proposer_slashings, _attester_slashings) = chain.op_pool.get_slashings(&state); - assert_eq!(proposer_slashings.len(), 0); - - let slot = state.slot; - let proposer_index = chain - .block_proposer(slot) - .expect("should get proposer index"); - let keypair = generate_deterministic_keypair(proposer_index); - let key = &keypair.sk; - let fork = &state.fork; - let proposer_slashing = build_proposer_slashing::<E>( - ProposerSlashingTestTask::Valid, - proposer_index as u64, - &key, - fork, - state.genesis_validators_root, - spec, - ); - - let result = env - .runtime() - .block_on( - remote_node - .http - .beacon() - .proposer_slashing(proposer_slashing.clone()), - ) - .expect("should fetch from http api"); - assert!(result, true); - - // Length should be just one as we've inserted only one proposer slashing - let (proposer_slashings, _attester_slashings) = chain.op_pool.get_slashings(&state); - assert_eq!(proposer_slashings.len(), 1); - assert_eq!(proposer_slashing.clone(), proposer_slashings[0]); - - let mut invalid_proposer_slashing = build_proposer_slashing::<E>( - ProposerSlashingTestTask::Valid, - proposer_index as u64, - &key, - fork, - state.genesis_validators_root, - spec, - ); - invalid_proposer_slashing.signed_header_2 = invalid_proposer_slashing.signed_header_1.clone(); - - let result = env.runtime().block_on( - remote_node - .http - .beacon() - .proposer_slashing(invalid_proposer_slashing), - ); - assert!(result.is_err()); - - // Length should still be one as we've inserted nothing since last time. - let (proposer_slashings, _attester_slashings) = chain.op_pool.get_slashings(&state); - assert_eq!(proposer_slashings.len(), 1); - assert_eq!(proposer_slashing, proposer_slashings[0]); -} - -#[test] -fn attester_slashing() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let chain = node - .client - .beacon_chain() - .expect("node should have beacon chain"); - - let state = chain - .head() - .expect("should have retrieved state") - .beacon_state; - let slot = state.slot; - let spec = &chain.spec; - - let proposer_index = chain - .block_proposer(slot) - .expect("should get proposer index"); - let keypair = generate_deterministic_keypair(proposer_index); - - let secret_keys = vec![&keypair.sk]; - let validator_indices = vec![proposer_index as u64]; - let fork = &state.fork; - - // Checking there are no attester slashings before insertion - let (_proposer_slashings, attester_slashings) = chain.op_pool.get_slashings(&state); - assert_eq!(attester_slashings.len(), 0); - - let attester_slashing = build_double_vote_attester_slashing( - AttesterSlashingTestTask::Valid, - &validator_indices[..], - &secret_keys[..], - fork, - state.genesis_validators_root, - spec, - ); - - let result = env - .runtime() - .block_on( - remote_node - .http - .beacon() - .attester_slashing(attester_slashing.clone()), - ) - .expect("should fetch from http api"); - assert!(result, true); - - // Length should be just one as we've inserted only one attester slashing - let (_proposer_slashings, attester_slashings) = chain.op_pool.get_slashings(&state); - assert_eq!(attester_slashings.len(), 1); - assert_eq!(attester_slashing, attester_slashings[0]); - - // Building an invalid attester slashing - let mut invalid_attester_slashing = build_double_vote_attester_slashing( - AttesterSlashingTestTask::Valid, - &validator_indices[..], - &secret_keys[..], - fork, - state.genesis_validators_root, - spec, - ); - invalid_attester_slashing.attestation_2 = invalid_attester_slashing.attestation_1.clone(); - - let result = env.runtime().block_on( - remote_node - .http - .beacon() - .attester_slashing(invalid_attester_slashing), - ); - result.unwrap_err(); - - // Length should still be one as we've failed to insert the attester slashing. - let (_proposer_slashings, attester_slashings) = chain.op_pool.get_slashings(&state); - assert_eq!(attester_slashings.len(), 1); - assert_eq!(attester_slashing, attester_slashings[0]); -} - -mod validator_attestation { - use super::*; - use http::StatusCode; - use node_test_rig::environment::Environment; - use remote_beacon_node::{Error::DidNotSucceed, HttpClient}; - use types::{Attestation, AttestationDuty, MinimalEthSpec}; - use url::Url; - - fn setup() -> ( - Environment<MinimalEthSpec>, - LocalBeaconNode<MinimalEthSpec>, - HttpClient<MinimalEthSpec>, - Url, - AttestationDuty, - ) { - let mut env = build_env(); - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - let client = remote_node.http.clone(); - let socket_addr = node - .client - .http_listen_addr() - .expect("A remote beacon node must have a http server"); - let url = Url::parse(&format!( - "http://{}:{}/validator/attestation", - socket_addr.ip(), - socket_addr.port() - )) - .expect("should be valid endpoint"); - - // Find a validator that has duties in the current slot of the chain. - let mut validator_index = 0; - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - let state = beacon_chain.head().expect("should get head").beacon_state; - let duties = loop { - let duties = state - .get_attestation_duties(validator_index, RelativeEpoch::Current) - .expect("should have attestation duties cache") - .expect("should have attestation duties"); - - if duties.slot == node.client.beacon_chain().unwrap().slot().unwrap() { - break duties; - } else { - validator_index += 1 - } - }; - - (env, node, client, url, duties) - } - - #[test] - fn requires_query_parameters() { - let (mut env, _node, client, url, _duties) = setup(); - - let attestation = env.runtime().block_on( - // query parameters are missing - client.json_get::<Attestation<MinimalEthSpec>>(url.clone(), vec![]), - ); - - assert_matches!( - attestation.expect_err("should not succeed"), - DidNotSucceed { status, body } => { - assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body, "URL query must be valid and contain at least one of the following keys: [\"slot\"]".to_owned()); - } - ); - } - - #[test] - fn requires_slot() { - let (mut env, _node, client, url, duties) = setup(); - - let attestation = env.runtime().block_on( - // `slot` is missing - client.json_get::<Attestation<MinimalEthSpec>>( - url.clone(), - vec![("committee_index".into(), format!("{}", duties.index))], - ), - ); - - assert_matches!( - attestation.expect_err("should not succeed"), - DidNotSucceed { status, body } => { - assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body, "URL query must be valid and contain at least one of the following keys: [\"slot\"]".to_owned()); - } - ); - } - - #[test] - fn requires_committee_index() { - let (mut env, _node, client, url, duties) = setup(); - - let attestation = env.runtime().block_on( - // `committee_index` is missing. - client.json_get::<Attestation<MinimalEthSpec>>( - url.clone(), - vec![("slot".into(), format!("{}", duties.slot))], - ), - ); - - assert_matches!( - attestation.expect_err("should not succeed"), - DidNotSucceed { status, body } => { - assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body, "URL query must be valid and contain at least one of the following keys: [\"committee_index\"]".to_owned()); - } - ); - } -} - -#[cfg(target_os = "linux")] -#[test] -fn get_health() { - let mut env = build_env(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - env.runtime() - .block_on(remote_node.http.node().get_health()) - .unwrap(); -} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 9f6ee79b1..fd838e033 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -142,7 +142,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http") .long("http") - .help("Enable RESTful HTTP API server. Disabled by default.") + .help("Enable the RESTful HTTP API server. Disabled by default.") .takes_value(false), ) .arg( @@ -169,6 +169,38 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("") .takes_value(true), ) + /* Prometheus metrics HTTP server related arguments */ + .arg( + Arg::with_name("metrics") + .long("metrics") + .help("Enable the Prometheus metrics HTTP server. Disabled by default.") + .takes_value(false), + ) + .arg( + Arg::with_name("metrics-address") + .long("metrics-address") + .value_name("ADDRESS") + .help("Set the listen address for the Prometheus metrics HTTP server.") + .default_value("127.0.0.1") + .takes_value(true), + ) + .arg( + Arg::with_name("metrics-port") + .long("metrics-port") + .value_name("PORT") + .help("Set the listen TCP port for the Prometheus metrics HTTP server.") + .default_value("5054") + .takes_value(true), + ) + .arg( + Arg::with_name("metrics-allow-origin") + .long("metrics-allow-origin") + .value_name("ORIGIN") + .help("Set the value of the Access-Control-Allow-Origin response HTTP header for the Prometheus metrics HTTP server. \ + Use * to allow any origin (not recommended in production)") + .default_value("") + .takes_value(true), + ) /* Websocket related arguments */ .arg( Arg::with_name("ws") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index aabdbb35c..ba2dbe21a 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -87,26 +87,26 @@ pub fn get_config<E: EthSpec>( */ if cli_args.is_present("staking") { - client_config.rest_api.enabled = true; + client_config.http_api.enabled = true; client_config.sync_eth1_chain = true; } /* - * Http server + * Http API server */ if cli_args.is_present("http") { - client_config.rest_api.enabled = true; + client_config.http_api.enabled = true; } if let Some(address) = cli_args.value_of("http-address") { - client_config.rest_api.listen_address = address + client_config.http_api.listen_addr = address .parse::<Ipv4Addr>() .map_err(|_| "http-address is not a valid IPv4 address.")?; } if let Some(port) = cli_args.value_of("http-port") { - client_config.rest_api.port = port + client_config.http_api.listen_port = port .parse::<u16>() .map_err(|_| "http-port is not a valid u16.")?; } @@ -117,7 +117,36 @@ pub fn get_config<E: EthSpec>( hyper::header::HeaderValue::from_str(allow_origin) .map_err(|_| "Invalid allow-origin value")?; - client_config.rest_api.allow_origin = allow_origin.to_string(); + client_config.http_api.allow_origin = Some(allow_origin.to_string()); + } + + /* + * Prometheus metrics HTTP server + */ + + if cli_args.is_present("metrics") { + client_config.http_metrics.enabled = true; + } + + if let Some(address) = cli_args.value_of("metrics-address") { + client_config.http_metrics.listen_addr = address + .parse::<Ipv4Addr>() + .map_err(|_| "metrics-address is not a valid IPv4 address.")?; + } + + if let Some(port) = cli_args.value_of("metrics-port") { + client_config.http_metrics.listen_port = port + .parse::<u16>() + .map_err(|_| "metrics-port is not a valid u16.")?; + } + + if let Some(allow_origin) = cli_args.value_of("metrics-allow-origin") { + // Pre-validate the config value to give feedback to the user on node startup, instead of + // as late as when the first API response is produced. + hyper::header::HeaderValue::from_str(allow_origin) + .map_err(|_| "Invalid allow-origin value")?; + + client_config.http_metrics.allow_origin = Some(allow_origin.to_string()); } // Log a warning indicating an open HTTP server if it wasn't specified explicitly @@ -125,7 +154,7 @@ pub fn get_config<E: EthSpec>( if cli_args.is_present("staking") { warn!( log, - "Running HTTP server on port {}", client_config.rest_api.port + "Running HTTP server on port {}", client_config.http_api.listen_port ); } @@ -219,7 +248,8 @@ pub fn get_config<E: EthSpec>( unused_port("tcp").map_err(|e| format!("Failed to get port for libp2p: {}", e))?; client_config.network.discovery_port = unused_port("udp").map_err(|e| format!("Failed to get port for discovery: {}", e))?; - client_config.rest_api.port = 0; + client_config.http_api.listen_port = 0; + client_config.http_metrics.listen_port = 0; client_config.websocket_server.port = 0; } @@ -230,6 +260,11 @@ pub fn get_config<E: EthSpec>( client_config.eth1.deposit_contract_address = format!("{:?}", eth2_testnet_config.deposit_contract_address()?); + let spec_contract_address = format!("{:?}", spec.deposit_contract_address); + if client_config.eth1.deposit_contract_address != spec_contract_address { + return Err("Testnet contract address does not match spec".into()); + } + client_config.eth1.deposit_contract_deploy_block = eth2_testnet_config.deposit_contract_deploy_block; client_config.eth1.lowest_cached_block_number = @@ -265,7 +300,7 @@ pub fn get_config<E: EthSpec>( }; let trimmed_graffiti_len = cmp::min(raw_graffiti.len(), GRAFFITI_BYTES_LEN); - client_config.graffiti[..trimmed_graffiti_len] + client_config.graffiti.0[..trimmed_graffiti_len] .copy_from_slice(&raw_graffiti[..trimmed_graffiti_len]); if let Some(max_skip_slots) = cli_args.value_of("max-skip-slots") { diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index a09f8c6cd..feff1e320 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -71,7 +71,6 @@ impl<E: EthSpec> ProductionBeaconNode<E> { context: RuntimeContext<E>, mut client_config: ClientConfig, ) -> Result<Self, String> { - let http_eth2_config = context.eth2_config().clone(); let spec = context.eth2_config().spec.clone(); let client_config_1 = client_config.clone(); let client_genesis = client_config.genesis.clone(); @@ -118,26 +117,22 @@ impl<E: EthSpec> ProductionBeaconNode<E> { builder.no_eth1_backend()? }; - let (builder, events) = builder + let (builder, _events) = builder .system_time_slot_clock()? .tee_event_handler(client_config.websocket_server.clone())?; // Inject the executor into the discv5 network config. client_config.network.discv5_config.executor = Some(Box::new(executor)); - let builder = builder + builder .build_beacon_chain()? .network(&client_config.network) .await? - .notifier()?; - - let builder = if client_config.rest_api.enabled { - builder.http_server(&client_config, &http_eth2_config, events)? - } else { - builder - }; - - Ok(Self(builder.build())) + .notifier()? + .http_api_config(client_config.http_api.clone()) + .http_metrics_config(client_config.http_metrics.clone()) + .build() + .map(Self) } pub fn into_inner(self) -> ProductionClient<E> { diff --git a/beacon_node/tests/test.rs b/beacon_node/tests/test.rs index a845acf04..7d860538f 100644 --- a/beacon_node/tests/test.rs +++ b/beacon_node/tests/test.rs @@ -3,6 +3,7 @@ use beacon_chain::StateSkipConfig; use node_test_rig::{ environment::{Environment, EnvironmentBuilder}, + eth2::types::StateId, testing_client_config, LocalBeaconNode, }; use types::{EthSpec, MinimalEthSpec, Slot}; @@ -34,10 +35,12 @@ fn http_server_genesis_state() { let node = build_node(&mut env); let remote_node = node.remote_node().expect("should produce remote node"); - let (api_state, _root) = env + let api_state = env .runtime() - .block_on(remote_node.http.beacon().get_state_by_slot(Slot::new(0))) - .expect("should fetch state from http api"); + .block_on(remote_node.get_debug_beacon_states(StateId::Slot(Slot::new(0)))) + .expect("should fetch state from http api") + .unwrap() + .data; let mut db_state = node .client diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 18e0ccad2..b570357b9 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -14,20 +14,15 @@ * [Key recovery](./key-recovery.md) * [Validator Management](./validator-management.md) * [Importing from the Eth2 Launchpad](./validator-import-launchpad.md) -* [Local Testnets](./local-testnets.md) -* [API](./api.md) - * [HTTP (RESTful JSON)](./http.md) - * [/node](./http/node.md) - * [/beacon](./http/beacon.md) - * [/validator](./http/validator.md) - * [/consensus](./http/consensus.md) - * [/network](./http/network.md) - * [/spec](./http/spec.md) - * [/advanced](./http/advanced.md) - * [/lighthouse](./http/lighthouse.md) - * [WebSocket](./websockets.md) +* [APIs](./api.md) + * [Beacon Node API](./api-bn.md) + * [/lighthouse](./api-lighthouse.md) + * [Validator Inclusion APIs](./validator-inclusion.md) + * [Validator Client API](./api-vc.md) + * [Prometheus Metrics](./advanced_metrics.md) * [Advanced Usage](./advanced.md) * [Database Configuration](./advanced_database.md) + * [Local Testnets](./local-testnets.md) * [Contributing](./contributing.md) * [Development Environment](./setup.md) * [FAQs](./faq.md) diff --git a/book/src/advanced_metrics.md b/book/src/advanced_metrics.md new file mode 100644 index 000000000..6c901862e --- /dev/null +++ b/book/src/advanced_metrics.md @@ -0,0 +1,34 @@ +# Prometheus Metrics + +Lighthouse provides an extensive suite of metrics and monitoring in the +[Prometheus](https://prometheus.io/docs/introduction/overview/) export format +via a HTTP server built into Lighthouse. + +These metrics are generally consumed by a Prometheus server and displayed via a +Grafana dashboard. These components are available in a docker-compose format at +[sigp/lighthouse-metrics](https://github.com/sigp/lighthouse-metrics). + +## Beacon Node Metrics + +By default, these metrics are disabled but can be enabled with the `--metrics` +flag. Use the `--metrics-address`, `--metrics-port` and +`--metrics-allow-origin` flags to customize the metrics server. + +### Example + +Start a beacon node with the metrics server enabled: + +```bash +lighthouse bn --metrics +``` + +Check to ensure that the metrics are available on the default port: + +```bash +curl localhost:5054/metrics +``` + +## Validator Client Metrics + +The validator client does not *yet* expose metrics, however this functionality +is expected to be implemented in late-September 2020. diff --git a/book/src/api-bn.md b/book/src/api-bn.md new file mode 100644 index 000000000..d957e4376 --- /dev/null +++ b/book/src/api-bn.md @@ -0,0 +1,130 @@ +# Beacon Node API + +Lighthouse implements the standard [Eth2 Beacon Node API +specification][OpenAPI]. Please follow that link for a full description of each API endpoint. + +> **Warning:** the standard API specification is still in flux and the Lighthouse implementation is partially incomplete. You can track the status of each endpoint at [#1434](https://github.com/sigp/lighthouse/issues/1434). + +## Starting the server + +A Lighthouse beacon node can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `127.0.0.1:5052`. + +The following CLI flags control the HTTP server: + +- `--http`: enable the HTTP server (required even if the following flags are + provided). +- `--http-port`: specify the listen port of the server. +- `--http-address`: specify the listen address of the server. +- `--http-allow-origin`: specify the value of the `Access-Control-Allow-Origin` + header. The default is to not supply a header. + +The schema of the API aligns with the standard Eth2 Beacon Node API as defined +at [github.com/ethereum/eth2.0-APIs](https://github.com/ethereum/eth2.0-APIs). +An interactive specification is available [here][OpenAPI]. + +### CLI Example + +Start the beacon node with the HTTP server listening on [http://localhost:5052](http://localhost:5052): + +```bash +lighthouse bn --http +``` + +## HTTP Request/Response Examples + +This section contains some simple examples of using the HTTP API via `curl`. +All endpoints are documented in the [Eth2 Beacon Node API +specification][OpenAPI]. + +### View the head of the beacon chain + +Returns the block header at the head of the canonical chain. + +```bash +curl -X GET "http://localhost:5052/eth/v1/beacon/headers/head" -H "accept: +application/json" +``` + +```json +{ + "data": { + "root": "0x4381454174fc28c7095077e959dcab407ae5717b5dca447e74c340c1b743d7b2", + "canonical": true, + "header": { + "message": { + "slot": 3199, + "proposer_index": "19077", + "parent_root": "0xf1934973041c5896d0d608e52847c3cd9a5f809c59c64e76f6020e3d7cd0c7cd", + "state_root": "0xe8e468f9f5961655dde91968f66480868dab8d4147de9498111df2b7e4e6fe60", + "body_root": "0x6f183abc6c4e97f832900b00d4e08d4373bfdc819055d76b0f4ff850f559b883" + }, + "signature": "0x988064a2f9cf13fe3aae051a3d85f6a4bca5a8ff6196f2f504e32f1203b549d5f86a39c6509f7113678880701b1881b50925a0417c1c88a750c8da7cd302dda5aabae4b941e3104d0cf19f5043c4f22a7d75d0d50dad5dbdaf6991381dc159ab" + } + } +} +``` + +### View the status of a validator + +Shows the status of validator at index `1` at the `head` state. + +```bash +curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "accept: application/json" +``` + +```json +{ + "data": { + "index": "1", + "balance": "63985937939", + "status": "Active", + "validator": { + "pubkey": "0x873e73ee8b3e4fcf1d2fb0f1036ba996ac9910b5b348f6438b5f8ef50857d4da9075d0218a9d1b99a9eae235a39703e1", + "withdrawal_credentials": "0x00b8cdcf79ba7e74300a07e9d8f8121dd0d8dd11dcfd6d3f2807c45b426ac968", + "effective_balance": 32000000000, + "slashed": false, + "activation_eligibility_epoch": 0, + "activation_epoch": 0, + "exit_epoch": 18446744073709552000, + "withdrawable_epoch": 18446744073709552000 + } + } +} +``` + +## Troubleshooting + +### HTTP API is unavailable or refusing connections + +Ensure the `--http` flag has been supplied at the CLI. + +You can quickly check that the HTTP endpoint is up using `curl`: + +```bash +curl -X GET "http://localhost:5052/eth/v1/node/version" -H "accept: application/json" +``` + +The beacon node should respond with its version: + +```json +{"data":{"version":"Lighthouse/v0.2.9-6f7b4768a/x86_64-linux"}} +``` + +If this doesn't work, the server might not be started or there might be a +network connection error. + +### I cannot query my node from a web browser (e.g., Swagger) + +By default, the API does not provide an `Access-Control-Allow-Origin` header, +which causes browsers to reject responses with a CORS error. + +The `--http-allow-origin` flag can be used to add a wild-card CORS header: + +```bash +lighthouse bn --http --http-allow-origin "*" +``` + +> **Warning:** Adding the wild-card allow-origin flag can pose a security risk. +> Only use it in production if you understand the risks of a loose CORS policy. + +[OpenAPI]: https://ethereum.github.io/eth2.0-APIs/#/ diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md new file mode 100644 index 000000000..3f37673fa --- /dev/null +++ b/book/src/api-lighthouse.md @@ -0,0 +1,179 @@ +# Lighthouse Non-Standard APIs + +Lighthouse fully supports the standardization efforts at +[github.com/ethereum/eth2.0-APIs](https://github.com/ethereum/eth2.0-APIs), +however sometimes development requires additional endpoints that shouldn't +necessarily be defined as a broad-reaching standard. Such endpoints are placed +behind the `/lighthouse` path. + +The endpoints behind the `/lighthouse` path are: + +- Not intended to be stable. +- Not guaranteed to be safe. +- For testing and debugging purposes only. + +Although we don't recommend that users rely on these endpoints, we +document them briefly so they can be utilized by developers and +researchers. + +### `/lighthouse/health` + +*Presently only available on Linux.* + +```bash +curl -X GET "http://localhost:5052/lighthouse/health" -H "accept: application/json" | jq +``` + +```json +{ + "data": { + "pid": 1728254, + "pid_num_threads": 47, + "pid_mem_resident_set_size": 510054400, + "pid_mem_virtual_memory_size": 3963158528, + "sys_virt_mem_total": 16715530240, + "sys_virt_mem_available": 4065374208, + "sys_virt_mem_used": 11383402496, + "sys_virt_mem_free": 1368662016, + "sys_virt_mem_percent": 75.67906, + "sys_loadavg_1": 4.92, + "sys_loadavg_5": 5.53, + "sys_loadavg_15": 5.58 + } +} +``` + +### `/lighthouse/syncing` + +```bash +curl -X GET "http://localhost:5052/lighthouse/syncing" -H "accept: application/json" | jq +``` + +```json +{ + "data": { + "SyncingFinalized": { + "start_slot": 3104, + "head_slot": 343744, + "head_root": "0x1b434b5ed702338df53eb5e3e24336a90373bb51f74b83af42840be7421dd2bf" + } + } +} +``` + +### `/lighthouse/peers` + +```bash +curl -X GET "http://localhost:5052/lighthouse/peers" -H "accept: application/json" | jq +``` + +```json +[ + { + "peer_id": "16Uiu2HAmA9xa11dtNv2z5fFbgF9hER3yq35qYNTPvN7TdAmvjqqv", + "peer_info": { + "_status": "Healthy", + "score": { + "score": 0 + }, + "client": { + "kind": "Lighthouse", + "version": "v0.2.9-1c9a055c", + "os_version": "aarch64-linux", + "protocol_version": "lighthouse/libp2p", + "agent_string": "Lighthouse/v0.2.9-1c9a055c/aarch64-linux" + }, + "connection_status": { + "status": "disconnected", + "connections_in": 0, + "connections_out": 0, + "last_seen": 1082, + "banned_ips": [] + }, + "listening_addresses": [ + "/ip4/80.109.35.174/tcp/9000", + "/ip4/127.0.0.1/tcp/9000", + "/ip4/192.168.0.73/tcp/9000", + "/ip4/172.17.0.1/tcp/9000", + "/ip6/::1/tcp/9000" + ], + "sync_status": { + "Advanced": { + "info": { + "status_head_slot": 343829, + "status_head_root": "0xe34e43efc2bb462d9f364bc90e1f7f0094e74310fd172af698b5a94193498871", + "status_finalized_epoch": 10742, + "status_finalized_root": "0x1b434b5ed702338df53eb5e3e24336a90373bb51f74b83af42840be7421dd2bf" + } + } + }, + "meta_data": { + "seq_number": 160, + "attnets": "0x0000000800000080" + } + } + } +] +``` + +### `/lighthouse/peers/connected` + +```bash +curl -X GET "http://localhost:5052/lighthouse/peers/connected" -H "accept: application/json" | jq +``` + +```json +[ + { + "peer_id": "16Uiu2HAkzJC5TqDSKuLgVUsV4dWat9Hr8EjNZUb6nzFb61mrfqBv", + "peer_info": { + "_status": "Healthy", + "score": { + "score": 0 + }, + "client": { + "kind": "Lighthouse", + "version": "v0.2.8-87181204+", + "os_version": "x86_64-linux", + "protocol_version": "lighthouse/libp2p", + "agent_string": "Lighthouse/v0.2.8-87181204+/x86_64-linux" + }, + "connection_status": { + "status": "connected", + "connections_in": 1, + "connections_out": 0, + "last_seen": 0, + "banned_ips": [] + }, + "listening_addresses": [ + "/ip4/34.204.178.218/tcp/9000", + "/ip4/127.0.0.1/tcp/9000", + "/ip4/172.31.67.58/tcp/9000", + "/ip4/172.17.0.1/tcp/9000", + "/ip6/::1/tcp/9000" + ], + "sync_status": "Unknown", + "meta_data": { + "seq_number": 1819, + "attnets": "0xffffffffffffffff" + } + } + } +] +``` + +### `/lighthouse/proto_array` + +```bash +curl -X GET "http://localhost:5052/lighthouse/proto_array" -H "accept: application/json" | jq +``` + +*Example omitted for brevity.* + +### `/lighthouse/validator_inclusion/{epoch}/{validator_id}` + +See [Validator Inclusion APIs](./validator-inclusion.md). + +### `/lighthouse/validator_inclusion/{epoch}/global` + +See [Validator Inclusion APIs](./validator-inclusion.md). diff --git a/book/src/api-vc.md b/book/src/api-vc.md new file mode 100644 index 000000000..e120f69bf --- /dev/null +++ b/book/src/api-vc.md @@ -0,0 +1,3 @@ +# Validator Client API + +The validator client API is planned for release in late September 2020. diff --git a/book/src/api.md b/book/src/api.md index 0fa6c3001..56c1ff5ce 100644 --- a/book/src/api.md +++ b/book/src/api.md @@ -1,13 +1,9 @@ # APIs -The Lighthouse `beacon_node` provides two APIs for local consumption: +Lighthouse allows users to query the state of Eth2.0 using web-standard, +RESTful HTTP/JSON APIs. -- A [RESTful JSON HTTP API](http.html) which provides beacon chain, node and network - information. -- A read-only [WebSocket API](websockets.html) providing beacon chain events, as they occur. +There are two APIs served by Lighthouse: - -## Security - -These endpoints are not designed to be exposed to the public Internet or -untrusted users. They may pose a considerable DoS attack vector when used improperly. +- [Beacon Node API](./api-bn.md) +- [Validator Client API](./api-vc.md) (not yet released). diff --git a/book/src/http.md b/book/src/http.md index e07440e8d..700535c2a 100644 --- a/book/src/http.md +++ b/book/src/http.md @@ -1,5 +1,9 @@ # HTTP API +[OpenAPI Specification](https://ethereum.github.io/eth2.0-APIs/#/) + +## Beacon Node + A Lighthouse beacon node can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `localhost:5052`. The following CLI flags control the HTTP server: @@ -9,24 +13,10 @@ The following CLI flags control the HTTP server: - `--http-port`: specify the listen port of the server. - `--http-address`: specify the listen address of the server. -The API is logically divided into several core endpoints, each documented in -detail: - -Endpoint | Description | -| --- | -- | -[`/node`](./http/node.md) | General information about the beacon node. -[`/beacon`](./http/beacon.md) | General information about the beacon chain. -[`/validator`](./http/validator.md) | Provides functionality to validator clients. -[`/consensus`](./http/consensus.md) | Proof-of-stake voting statistics. -[`/network`](./http/network.md) | Information about the p2p network. -[`/spec`](./http/spec.md) | Information about the specs that the client is running. -[`/advanced`](./http/advanced.md) | Provides endpoints for advanced inspection of Lighthouse specific objects. -[`/lighthouse`](./http/lighthouse.md) | Provides lighthouse specific endpoints. - -_Please note: The OpenAPI format at -[SwaggerHub: Lighthouse REST -API](https://app.swaggerhub.com/apis-docs/spble/lighthouse_rest_api/0.2.0) has -been **deprecated**. This documentation is now the source of truth for the REST API._ +The schema of the API aligns with the standard Eth2 Beacon Node API as defined +at [github.com/ethereum/eth2.0-APIs](https://github.com/ethereum/eth2.0-APIs). +It is an easy-to-use RESTful HTTP/JSON API. An interactive specification is +available [here](https://ethereum.github.io/eth2.0-APIs/#/). ## Troubleshooting diff --git a/book/src/http/advanced.md b/book/src/http/advanced.md deleted file mode 100644 index 822b6ffff..000000000 --- a/book/src/http/advanced.md +++ /dev/null @@ -1,115 +0,0 @@ -# Lighthouse REST API: `/advanced` - -The `/advanced` endpoints provide information Lighthouse specific data structures for advanced debugging. - -## Endpoints - -HTTP Path | Description | -| --- | -- | -[`/advanced/fork_choice`](#advancedfork_choice) | Get the `proto_array` fork choice object. -[`/advanced/operation_pool`](#advancedoperation_pool) | Get the Lighthouse `PersistedOperationPool` object. - - -## `/advanced/fork_choice` - -Requests the `proto_array` fork choice object as represented in Lighthouse. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/advanced/fork_choice` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - "prune_threshold": 256, - "justified_epoch": 25, - "finalized_epoch": 24, - "nodes": [ - { - "slot": 544, - "root": "0x27103c56d4427cb4309dd202920ead6381d54d43277c29cf0572ddf0d528e6ea", - "parent": null, - "justified_epoch": 16, - "finalized_epoch": 15, - "weight": 256000000000, - "best_child": 1, - "best_descendant": 296 - }, - { - "slot": 545, - "root": "0x09af0e8d4e781ea4280c9c969d168839c564fab3a03942e7db0bfbede7d4c745", - "parent": 0, - "justified_epoch": 16, - "finalized_epoch": 15, - "weight": 256000000000, - "best_child": 2, - "best_descendant": 296 - }, - ], - "indices": { - "0xb935bb3651eeddcb2d2961bf307156850de982021087062033f02576d5df00a3": 59, - "0x8f4ec47a34c6c1d69ede64d27165d195f7e2a97c711808ce51f1071a6e12d5b9": 189, - "0xf675eba701ef77ee2803a130dda89c3c5673a604d2782c9e25ea2be300d7d2da": 173, - "0x488a483c8d5083faaf5f9535c051b9f373ba60d5a16e77ddb1775f248245b281": 37 - } -} -``` -_Truncated for brevity._ - -## `/advanced/operation_pool` - -Requests the `PersistedOperationPool` object as represented in Lighthouse. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/advanced/operation_pool` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - "attestations": [ - [ - { - "v": [39, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 118, 215, 252, 51, 186, 76, 156, 157, 99, 91, 4, 137, 195, 209, 224, 26, 233, 233, 184, 38, 89, 215, 177, 247, 97, 243, 119, 229, 69, 50, 90, 24, 0, 0, 0, 0, 0, 0, 0, 79, 37, 38, 210, 96, 235, 121, 142, 129, 136, 206, 214, 179, 132, 22, 19, 222, 213, 203, 46, 112, 192, 26, 5, 254, 26, 103, 170, 158, 205, 72, 3, 25, 0, 0, 0, 0, 0, 0, 0, 164, 50, 214, 67, 98, 13, 50, 180, 108, 232, 248, 109, 128, 45, 177, 23, 221, 24, 218, 211, 8, 152, 172, 120, 24, 86, 198, 103, 68, 164, 67, 202, 1, 0, 0, 0, 0, 0, 0, 0] - }, - [ - { - "aggregation_bits": "0x03", - "data": { - "slot": 807, - "index": 0, - "beacon_block_root": "0x7076d7fc33ba4c9c9d635b0489c3d1e01ae9e9b82659d7b1f761f377e545325a", - "source": { - "epoch": 24, - "root": "0x4f2526d260eb798e8188ced6b3841613ded5cb2e70c01a05fe1a67aa9ecd4803" - }, - "target": { - "epoch": 25, - "root": "0xa432d643620d32b46ce8f86d802db117dd18dad30898ac781856c66744a443ca" - } - }, - "signature": "0x8b1d624b0cd5a7a0e13944e90826878a230e3901db34ea87dbef5b145ade2fedbc830b6752a38a0937a1594211ab85b615d65f9eef0baccd270acca945786036695f4db969d9ff1693c505c0fe568b2fe9831ea78a74cbf7c945122231f04026" - } - ] - ] - ], - "attester_slashings": [], - "proposer_slashings": [], - "voluntary_exits": [] -} -``` -_Truncated for brevity._ diff --git a/book/src/http/beacon.md b/book/src/http/beacon.md deleted file mode 100644 index 2149f4444..000000000 --- a/book/src/http/beacon.md +++ /dev/null @@ -1,784 +0,0 @@ -# Lighthouse REST API: `/beacon` - -The `/beacon` endpoints provide information about the canonical head of the -beacon chain and also historical information about beacon blocks and states. - -## Endpoints - -HTTP Path | Description | -| --- | -- | -[`/beacon/head`](#beaconhead) | Info about the block at the head of the chain. -[`/beacon/heads`](#beaconheads) | Returns a list of all known chain heads. -[`/beacon/block`](#beaconblock) | Get a `BeaconBlock` by slot or root. -[`/beacon/block_root`](#beaconblock_root) | Resolve a slot to a block root. -[`/beacon/fork`](#beaconfork) | Get the fork of the head of the chain. -[`/beacon/genesis_time`](#beacongenesis_time) | Get the genesis time from the beacon state. -[`/beacon/genesis_validators_root`](#beacongenesis_validators_root) | Get the genesis validators root. -[`/beacon/validators`](#beaconvalidators) | Query for one or more validators. -[`/beacon/validators/all`](#beaconvalidatorsall) | Get all validators. -[`/beacon/validators/active`](#beaconvalidatorsactive) | Get all active validators. -[`/beacon/state`](#beaconstate) | Get a `BeaconState` by slot or root. -[`/beacon/state_root`](#beaconstate_root) | Resolve a slot to a state root. -[`/beacon/state/genesis`](#beaconstategenesis) | Get a `BeaconState` at genesis. -[`/beacon/committees`](#beaconcommittees) | Get the shuffling for an epoch. -[`/beacon/proposer_slashing`](#beaconproposer_slashing) | Insert a proposer slashing -[`/beacon/attester_slashing`](#beaconattester_slashing) | Insert an attester slashing - -## `/beacon/head` - -Requests information about the head of the beacon chain, from the node's -perspective. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/head` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - "slot": 37923, - "block_root": "0xe865d4805395a0776b8abe46d714a9e64914ab8dc5ff66624e5a1776bcc1684b", - "state_root": "0xe500e3567ab273c9a6f8a057440deff476ab236f0983da27f201ee9494a879f0", - "finalized_slot": 37856, - "finalized_block_root": "0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86", - "justified_slot": 37888, - "justified_block_root": "0x01c2f516a407d8fdda23cad4ed4381e4ab8913d638f935a2fe9bd00d6ced5ec4", - "previous_justified_slot": 37856, - "previous_justified_block_root": "0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86" -} -``` - -## `/beacon/heads` - -Returns the roots of all known head blocks. Only one of these roots is the -canonical head and that is decided by the fork choice algorithm. See [`/beacon/head`](#beaconhead) for the canonical head. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/heads` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -[ - { - "beacon_block_root": "0x226b2fd7c5f3d31dbb21444b96dfafe715f0017cd16545ecc4ffa87229496a69", - "beacon_block_slot": 38373 - }, - { - "beacon_block_root": "0x41ed5b253c4fc841cba8a6d44acbe101866bc674c3cfa3c4e9f7388f465aa15b", - "beacon_block_slot": 38375 - } -] -``` - -## `/beacon/block` - -Request that the node return a beacon chain block that matches the provided -criteria (a block `root` or beacon chain `slot`). Only one of the parameters -should be provided as a criteria. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/block` -Method | GET -JSON Encoding | Object -Query Parameters | `slot`, `root` -Typical Responses | 200, 404 - -### Parameters - -Accepts **only one** of the following parameters: - -- `slot` (`Slot`): Query by slot number. Any block returned must be in the canonical chain (i.e., -either the head or an ancestor of the head). -- `root` (`Bytes32`): Query by tree hash root. A returned block is not required to be in the -canonical chain. - -### Returns - -Returns an object containing a single [`SignedBeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/v0.10.0/specs/phase0/beacon-chain.md#signedbeaconblock) and the block root of the inner [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/v0.10.0/specs/phase0/beacon-chain.md#beaconblock). - -### Example Response - -```json -{ - "root": "0xc35ddf4e71c31774e0594bd7eb32dfe50b54dbc40abd594944254b4ec8895196", - "beacon_block": { - "message": { - "slot": 0, - "proposer_index": 14, - "parent_root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "state_root": "0xf15690b6be4ed42ea1ee0741eb4bfd4619d37be8229b84b4ddd480fb028dcc8f", - "body": { - "randao_reveal": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "eth1_data": { - "deposit_root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "deposit_count": 0, - "block_hash": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - "graffiti": "0x0000000000000000000000000000000000000000000000000000000000000000", - "proposer_slashings": [], - "attester_slashings": [], - "attestations": [], - "deposits": [], - "voluntary_exits": [] - } - }, - "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - } -} -``` - -## `/beacon/block_root` - -Returns the block root for the given slot in the canonical chain. If there -is a re-org, the same slot may return a different root. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/block_root` -Method | GET -JSON Encoding | Object -Query Parameters | `slot` -Typical Responses | 200, 404 - -## Parameters - -- `slot` (`Slot`): the slot to be resolved to a root. - -### Example Response - -```json -"0xc35ddf4e71c31774e0594bd7eb32dfe50b54dbc40abd594944254b4ec8895196" -``` - -## `/beacon/committees` - -Request the committees (a.k.a. "shuffling") for all slots and committee indices -in a given `epoch`. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/committees` -Method | GET -JSON Encoding | Object -Query Parameters | `epoch` -Typical Responses | 200/500 - -### Parameters - -The `epoch` (`Epoch`) query parameter is required and defines the epoch for -which the committees will be returned. All slots contained within the response will -be inside this epoch. - -### Returns - -A list of beacon committees. - -### Example Response - -```json -[ - { - "slot": 4768, - "index": 0, - "committee": [ - 1154, - 492, - 9667, - 3089, - 8987, - 1421, - 224, - 11243, - 2127, - 2329, - 188, - 482, - 486 - ] - }, - { - "slot": 4768, - "index": 1, - "committee": [ - 5929, - 8482, - 5528, - 6130, - 14343, - 9777, - 10808, - 12739, - 15234, - 12819, - 5423, - 6320, - 9991 - ] - } -] -``` - -_Truncated for brevity._ - -## `/beacon/fork` - -Request that the node return the `fork` of the current head. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/fork` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - - -### Returns - -Returns an object containing the [`Fork`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#fork) of the current head. - -### Example Response - -```json -{ - "previous_version": "0x00000000", - "current_version": "0x00000000", - "epoch": 0 -} -``` - -## `/beacon/genesis_time` - -Request that the node return the genesis time from the beacon state. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/genesis_time` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - - -### Returns - -Returns an object containing the genesis time. - -### Example Response - -```json -1581576353 -``` - -## `/beacon/genesis_validators_root` - -Request that the node return the genesis validators root from the beacon state. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/genesis_validators_root` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - - -### Returns - -Returns an object containing the genesis validators root. - -### Example Response - -```json -0x4fbf23439a7a9b9dd91650e64e8124012dde5e2ea2940c552b86f04eb47f95de -``` - -## `/beacon/validators` - -Request that the node returns information about one or more validator public -keys. This request takes the form of a `POST` request to allow sending a large -number of pubkeys in the request. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/validators` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Request Body - -Expects the following object in the POST request body: - -``` -{ - state_root: Bytes32, - pubkeys: [PublicKey] -} -``` - -The `state_root` field indicates which `BeaconState` should be used to collect -the information. The `state_root` is optional and omitting it will result in -the canonical head state being used. - - -### Returns - -Returns an object describing several aspects of the given validator. - -### Example - -### Request Body - -```json -{ - "pubkeys": [ - "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42" - ] -} -``` - -_Note: for demonstration purposes the second pubkey is some unknown pubkey._ - -### Response Body - -```json -[ - { - "pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "validator_index": 14935, - "balance": 3228885987, - "validator": { - "pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "withdrawal_credentials": "0x00b7bec22d5bda6b2cca1343d4f640d0e9ccc204a06a73703605c590d4c0d28e", - "effective_balance": 3200000000, - "slashed": false, - "activation_eligibility_epoch": 0, - "activation_epoch": 0, - "exit_epoch": 18446744073709551615, - "withdrawable_epoch": 18446744073709551615 - } - }, - { - "pubkey": "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42", - "validator_index": null, - "balance": null, - "validator": null - } -] -``` - -## `/beacon/validators/all` - -Returns all validators. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/validators/all` -Method | GET -JSON Encoding | Object -Query Parameters | `state_root` (optional) -Typical Responses | 200 - -### Parameters - -The optional `state_root` (`Bytes32`) query parameter indicates which -`BeaconState` should be used to collect the information. When omitted, the -canonical head state will be used. - -### Returns - -The return format is identical to the [`/beacon/validators`](#beaconvalidators) response body. - - -## `/beacon/validators/active` - -Returns all validators that are active in the state defined by `state_root`. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/validators/active` -Method | GET -JSON Encoding | Object -Query Parameters | `state_root` (optional) -Typical Responses | 200 - -### Parameters - -The optional `state_root` (`Bytes32`) query parameter indicates which -`BeaconState` should be used to collect the information. When omitted, the -canonical head state will be used. - -### Returns - -The return format is identical to the [`/beacon/validators`](#beaconvalidators) response body. - - -## `/beacon/state` - -Request that the node return a beacon chain state that matches the provided -criteria (a state `root` or beacon chain `slot`). Only one of the parameters -should be provided as a criteria. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/state` -Method | GET -JSON Encoding | Object -Query Parameters | `slot`, `root` -Typical Responses | 200, 404 - -### Parameters - -Accepts **only one** of the following parameters: - -- `slot` (`Slot`): Query by slot number. Any state returned must be in the canonical chain (i.e., -either the head or an ancestor of the head). -- `root` (`Bytes32`): Query by tree hash root. A returned state is not required to be in the -canonical chain. - -### Returns - -Returns an object containing a single -[`BeaconState`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beaconstate) -and its tree hash root. - -### Example Response - -```json -{ - "root": "0x528e54ca5d4c957729a73f40fc513ae312e054c7295775c4a2b21f423416a72b", - "beacon_state": { - "genesis_time": 1575652800, - "genesis_validators_root": "0xa8a9226edee1b2627fb4117d7dea4996e64dec2998f37f6e824f74f2ce39a538", - "slot": 18478 - } -} -``` - -_Truncated for brevity._ - -## `/beacon/state_root` - -Returns the state root for the given slot in the canonical chain. If there -is a re-org, the same slot may return a different root. - - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/state_root` -Method | GET -JSON Encoding | Object -Query Parameters | `slot` -Typical Responses | 200, 404 - -## Parameters - -- `slot` (`Slot`): the slot to be resolved to a root. - -### Example Response - -```json -"0xf15690b6be4ed42ea1ee0741eb4bfd4619d37be8229b84b4ddd480fb028dcc8f" -``` - -## `/beacon/state/genesis` - -Request that the node return a beacon chain state at genesis (slot 0). - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/state/genesis` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - - -### Returns - -Returns an object containing the genesis -[`BeaconState`](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beaconstate). - -### Example Response - -```json -{ - "genesis_time": 1581576353, - "slot": 0, - "fork": { - "previous_version": "0x00000000", - "current_version": "0x00000000", - "epoch": 0 - }, -} -``` - -_Truncated for brevity._ - - -## `/beacon/state/committees` - -Request that the node return a beacon chain state at genesis (slot 0). - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/state/genesis` -Method | GET -JSON Encoding | Object -Query Parameters | `epoch` -Typical Responses | 200 - - -### Returns - -Returns an object containing the committees for a given epoch. - -### Example Response - -```json -[ - {"slot":64,"index":0,"committee":[]}, - {"slot":65,"index":0,"committee":[3]}, - {"slot":66,"index":0,"committee":[]}, - {"slot":67,"index":0,"committee":[14]}, - {"slot":68,"index":0,"committee":[]}, - {"slot":69,"index":0,"committee":[9]}, - {"slot":70,"index":0,"committee":[]}, - {"slot":71,"index":0,"committee":[11]}, - {"slot":72,"index":0,"committee":[]}, - {"slot":73,"index":0,"committee":[5]}, - {"slot":74,"index":0,"committee":[]}, - {"slot":75,"index":0,"committee":[15]}, - {"slot":76,"index":0,"committee":[]}, - {"slot":77,"index":0,"committee":[0]} -] -``` - -_Truncated for brevity._ - - -## `/beacon/attester_slashing` - -Accepts an `attester_slashing` and verifies it. If it is valid, it is added to the operations pool for potential inclusion in a future block. Returns a 400 error if the `attester_slashing` is invalid. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/attester_slashing` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200/400 - -### Parameters - -Expects the following object in the POST request body: - -``` -{ - attestation_1: { - attesting_indices: [u64], - data: { - slot: Slot, - index: u64, - beacon_block_root: Bytes32, - source: { - epoch: Epoch, - root: Bytes32 - }, - target: { - epoch: Epoch, - root: Bytes32 - } - } - signature: Bytes32 - }, - attestation_2: { - attesting_indices: [u64], - data: { - slot: Slot, - index: u64, - beacon_block_root: Bytes32, - source: { - epoch: Epoch, - root: Bytes32 - }, - target: { - epoch: Epoch, - root: Bytes32 - } - } - signature: Bytes32 - } -} -``` - -### Returns - -Returns `true` if the attester slashing was inserted successfully, or the corresponding error if it failed. - -### Example - -### Request Body - -```json -{ - "attestation_1": { - "attesting_indices": [0], - "data": { - "slot": 1, - "index": 0, - "beacon_block_root": "0x0000000000000000000000000000000000000000000000000100000000000000", - "source": { - "epoch": 1, - "root": "0x0000000000000000000000000000000000000000000000000100000000000000" - }, - "target": { - "epoch": 1, - "root": "0x0000000000000000000000000000000000000000000000000100000000000000" - } - }, - "signature": "0xb47f7397cd944b8d5856a13352166bbe74c85625a45b14b7347fc2c9f6f6f82acee674c65bc9ceb576fcf78387a6731c0b0eb3f8371c70db2da4e7f5dfbc451730c159d67263d3db56b6d0e009e4287a8ba3efcacac30b3ae3447e89dc71b5b9" - }, - "attestation_2": { - "attesting_indices": [0], - "data": { - "slot": 1, - "index": 0, - "beacon_block_root": "0x0000000000000000000000000000000000000000000000000100000000000000", - "source": { - "epoch": 1, - "root": "0x0000000000000000000000000000000000000000000000000100000000000000" - }, - "target": { - "epoch": 1, - "root": "0x0000000000000000000000000000000000000000000000000200000000000000" - } - }, - "signature": "0x93fef587a63acf72aaf8df627718fd43cb268035764071f802ffb4370a2969d226595cc650f4c0bf2291ae0c0a41fcac1700f318603d75d34bcb4b9f4a8368f61eeea0e1f5d969d92d5073ba5fbadec102b45ec87d418d25168d2e3c74b9fcbb" - } -} -``` - -_Note: data sent here is for demonstration purposes only_ - - - -## `/beacon/proposer_slashing` - -Accepts a `proposer_slashing` and verifies it. If it is valid, it is added to the operations pool for potential inclusion in a future block. Returns an 400 error if the `proposer_slashing` is invalid. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/beacon/proposer_slashing` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200/400 - -### Request Body - -Expects the following object in the POST request body: - -``` -{ - proposer_index: u64, - header_1: { - slot: Slot, - parent_root: Bytes32, - state_root: Bytes32, - body_root: Bytes32, - signature: Bytes32 - }, - header_2: { - slot: Slot, - parent_root: Bytes32, - state_root: Bytes32, - body_root: Bytes32, - signature: Bytes32 - } -} -``` - -### Returns - -Returns `true` if the proposer slashing was inserted successfully, or the corresponding error if it failed. - -### Example - -### Request Body - -```json -{ - "proposer_index": 0, - "header_1": { - "slot": 0, - "parent_root": "0x0101010101010101010101010101010101010101010101010101010101010101", - "state_root": "0x0101010101010101010101010101010101010101010101010101010101010101", - "body_root": "0x0101010101010101010101010101010101010101010101010101010101010101", - "signature": "0xb8970d1342c6d5779c700ec366efd0ca819937ca330960db3ca5a55eb370a3edd83f4cbb2f74d06e82f934fcbd4bb80609a19c2254cc8b3532a4efff9e80edf312ac735757c059d77126851e377f875593e64ba50d1dffe69a809a409202dd12" - }, - "header_2": { - "slot": 0, - "parent_root": "0x0202020202020202020202020202020202020202020202020202020202020202", - "state_root": "0x0101010101010101010101010101010101010101010101010101010101010101", - "body_root": "0x0101010101010101010101010101010101010101010101010101010101010101", - "signature": "0xb60e6b348698a34e59b22e0af96f8809f977f00f95d52375383ade8d22e9102270a66c6d52b0434214897e11ca4896871510c01b3fd74d62108a855658d5705fcfc4ced5136264a1c6496f05918576926aa191b1ad311b7e27f5aa2167aba294" - } -} -``` - -_Note: data sent here is for demonstration purposes only_ - - - - - diff --git a/book/src/http/lighthouse.md b/book/src/http/lighthouse.md deleted file mode 100644 index d80c0f694..000000000 --- a/book/src/http/lighthouse.md +++ /dev/null @@ -1,182 +0,0 @@ -# Lighthouse REST API: `/lighthouse` - -The `/lighthouse` endpoints provide lighthouse-specific information about the beacon node. - -## Endpoints - -HTTP Path | Description | -| --- | -- | -[`/lighthouse/syncing`](#lighthousesyncing) | Get the node's syncing status -[`/lighthouse/peers`](#lighthousepeers) | Get the peers info known by the beacon node -[`/lighthouse/connected_peers`](#lighthousepeers) | Get the connected_peers known by the beacon node - -## `/lighthouse/syncing` - -Requests the syncing state of a Lighthouse beacon node. Lighthouse as a -custom sync protocol, this request gets Lighthouse-specific sync information. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/lighthouse/syncing` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -If the node is undergoing a finalization sync: -```json -{ - "SyncingFinalized": { - "start_slot": 10, - "head_slot": 20, - "head_root":"0x74020d0e3c3c02d2ea6279d5760f7d0dd376c4924beaaec4d5c0cefd1c0c4465" - } -} -``` - -If the node is undergoing a head chain sync: -```json -{ - "SyncingHead": { - "start_slot":0, - "head_slot":1195 - } -} -``` - -If the node is synced -```json -{ -"Synced" -} -``` - -## `/lighthouse/peers` - -Get all known peers info from the beacon node. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/lighthouse/peers` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -[ -{ - "peer_id" : "16Uiu2HAmTEinipUS3haxqucrn7d7SmCKx5XzAVbAZCiNW54ncynG", - "peer_info" : { - "_status" : "Healthy", - "client" : { - "agent_string" : "github.com/libp2p/go-libp2p", - "kind" : "Prysm", - "os_version" : "unknown", - "protocol_version" : "ipfs/0.1.0", - "version" : "unknown" - }, - "connection_status" : { - "Disconnected" : { - "since" : 3 - } - }, - "listening_addresses" : [ - "/ip4/10.3.58.241/tcp/9001", - "/ip4/35.172.14.146/tcp/9001", - "/ip4/35.172.14.146/tcp/9001" - ], - "meta_data" : { - "attnets" : "0x0000000000000000", - "seq_number" : 0 - }, - "reputation" : 20, - "sync_status" : { - "Synced" : { - "status_head_slot" : 18146 - } - } - } - }, - { - "peer_id" : "16Uiu2HAm8XZfPv3YjktCjitSRtfS7UfHfEvpiUyHrdiX6uAD55xZ", - "peer_info" : { - "_status" : "Healthy", - "client" : { - "agent_string" : null, - "kind" : "Unknown", - "os_version" : "unknown", - "protocol_version" : "unknown", - "version" : "unknown" - }, - "connection_status" : { - "Disconnected" : { - "since" : 5 - } - }, - "listening_addresses" : [], - "meta_data" : { - "attnets" : "0x0900000000000000", - "seq_number" : 0 - }, - "reputation" : 20, - "sync_status" : "Unknown" - } - }, -] -``` - -## `/lighthouse/connected_peers` - -Get all known peers info from the beacon node. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/lighthouse/connected_peers` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -[ - { - "peer_id" : "16Uiu2HAm8XZfPv3YjktCjitSRtfS7UfHfEvpiUyHrdiX6uAD55xZ", - "peer_info" : { - "_status" : "Healthy", - "client" : { - "agent_string" : null, - "kind" : "Unknown", - "os_version" : "unknown", - "protocol_version" : "unknown", - "version" : "unknown" - }, - "connection_status" : { - "Connected" : { - "in" : 5, - "out" : 2 - } - }, - "listening_addresses" : [], - "meta_data" : { - "attnets" : "0x0900000000000000", - "seq_number" : 0 - }, - "reputation" : 20, - "sync_status" : "Unknown" - } - }, - ] -``` diff --git a/book/src/http/network.md b/book/src/http/network.md deleted file mode 100644 index 2ac0c83ba..000000000 --- a/book/src/http/network.md +++ /dev/null @@ -1,148 +0,0 @@ -# Lighthouse REST API: `/network` - -The `/network` endpoints provide information about the p2p network that -Lighthouse uses to communicate with other beacon nodes. - -## Endpoints - -HTTP Path | Description | -| --- | -- | -[`/network/enr`](#networkenr) | Get the local node's `ENR` as base64 . -[`/network/peer_count`](#networkpeer_count) | Get the count of connected peers. -[`/network/peer_id`](#networkpeer_id) | Get a node's libp2p `PeerId`. -[`/network/peers`](#networkpeers) | List a node's connected peers (as `PeerIds`). -[`/network/listen_port`](#networklisten_port) | Get a node's libp2p listening port. -[`/network/listen_addresses`](#networklisten_addresses) | Get a list of libp2p multiaddr the node is listening on. - -## `network/enr` - -Requests the beacon node for its listening `ENR` address. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/network/enr` -Method | GET -JSON Encoding | String (base64) -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -"-IW4QPYyGkXJSuJ2Eji8b-m4PTNrW4YMdBsNOBrYAdCk8NLMJcddAiQlpcv6G_hdNjiLACOPTkqTBhUjnC0wtIIhyQkEgmlwhKwqAPqDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhA1sBKo0yCfw4Z_jbggwflNfftjwKACu-a-CoFAQHJnrm" -``` - -## `/network/peer_count` - -Requests the count of peers connected to the client. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/network/peer_count` -Method | GET -JSON Encoding | Number -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -5 -``` -## `/network/peer_id` - -Requests the beacon node's local `PeerId`. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/network/peer_id` -Method | GET -JSON Encoding | String (base58) -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -"QmVFcULBYZecPdCKgGmpEYDqJLqvMecfhJadVBtB371Avd" -``` - -## `/network/peers` - -Requests one `MultiAddr` for each peer connected to the beacon node. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/network/peers` -Method | GET -JSON Encoding | [String] (base58) -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -[ - "QmaPGeXcfKFMU13d8VgbnnpeTxcvoFoD9bUpnRGMUJ1L9w", - "QmZt47cP8V96MgiS35WzHKpPbKVBMqr1eoBNTLhQPqpP3m" -] -``` - - -## `/network/listen_port` - -Requests the TCP port that the client's libp2p service is listening on. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/network/listen_port` -Method | GET -JSON Encoding | Number -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -9000 -``` - -## `/network/listen_addresses` - -Requests the list of multiaddr that the client's libp2p service is listening on. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/network/listen_addresses` -Method | GET -JSON Encoding | Array -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -[ - "/ip4/127.0.0.1/tcp/9000", - "/ip4/192.168.31.115/tcp/9000", - "/ip4/172.24.0.1/tcp/9000", - "/ip4/172.21.0.1/tcp/9000", - "/ip4/172.17.0.1/tcp/9000", - "/ip4/172.18.0.1/tcp/9000", - "/ip4/172.19.0.1/tcp/9000", - "/ip4/172.42.0.1/tcp/9000", - "/ip6/::1/tcp/9000" -] -``` diff --git a/book/src/http/node.md b/book/src/http/node.md deleted file mode 100644 index ae370cbe9..000000000 --- a/book/src/http/node.md +++ /dev/null @@ -1,91 +0,0 @@ -# Lighthouse REST API: `/node` - -The `/node` endpoints provide information about the lighthouse beacon node. - -## Endpoints - -HTTP Path | Description | -| --- | -- | -[`/node/version`](#nodeversion) | Get the node's version. -[`/node/syncing`](#nodesyncing) | Get the node's syncing status. -[`/node/health`](#nodehealth) | Get the node's health. - -## `/node/version` - -Requests the beacon node's version. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/node/version` -Method | GET -JSON Encoding | String -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -"Lighthouse-0.2.0-unstable" -``` - -## `/node/syncing` - -Requests the syncing status of the beacon node. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/node/syncing` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - is_syncing: true, - sync_status: { - starting_slot: 0, - current_slot: 100, - highest_slot: 200, - } -} -``` - -## `/node/health` - -Requests information about the health of the beacon node. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/node/health` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - "pid": 96160, - "pid_num_threads": 30, - "pid_mem_resident_set_size": 55476224, - "pid_mem_virtual_memory_size": 2081382400, - "sys_virt_mem_total": 16721076224, - "sys_virt_mem_available": 7423197184, - "sys_virt_mem_used": 8450183168, - "sys_virt_mem_free": 3496345600, - "sys_virt_mem_percent": 55.605743, - "sys_loadavg_1": 1.56, - "sys_loadavg_5": 2.61, - "sys_loadavg_15": 2.43 -} -``` diff --git a/book/src/http/spec.md b/book/src/http/spec.md deleted file mode 100644 index 619a1d4e3..000000000 --- a/book/src/http/spec.md +++ /dev/null @@ -1,154 +0,0 @@ -# Lighthouse REST API: `/spec` - -The `/spec` endpoints provide information about Eth2.0 specifications that the node is running. - -## Endpoints - -HTTP Path | Description | -| --- | -- | -[`/spec`](#spec) | Get the full spec object that a node's running. -[`/spec/slots_per_epoch`](#specslots_per_epoch) | Get the number of slots per epoch. -[`/spec/eth2_config`](#specseth2_config) | Get the full Eth2 config object. - -## `/spec` - -Requests the full spec object that a node's running. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/spec` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - "genesis_slot": 0, - "base_rewards_per_epoch": 4, - "deposit_contract_tree_depth": 32, - "max_committees_per_slot": 64, - "target_committee_size": 128, - "min_per_epoch_churn_limit": 4, - "churn_limit_quotient": 65536, - "shuffle_round_count": 90, - "min_genesis_active_validator_count": 16384, - "min_genesis_time": 1578009600, - "min_deposit_amount": 1000000000, - "max_effective_balance": 32000000000, - "ejection_balance": 16000000000, - "effective_balance_increment": 1000000000, - "genesis_fork_version": "0x00000000", - "bls_withdrawal_prefix_byte": "0x00", - "genesis_delay": 172800, - "milliseconds_per_slot": 12000, - "min_attestation_inclusion_delay": 1, - "min_seed_lookahead": 1, - "max_seed_lookahead": 4, - "min_epochs_to_inactivity_penalty": 4, - "min_validator_withdrawability_delay": 256, - "shard_committee_period": 2048, - "base_reward_factor": 64, - "whistleblower_reward_quotient": 512, - "proposer_reward_quotient": 8, - "inactivity_penalty_quotient": 33554432, - "min_slashing_penalty_quotient": 32, - "domain_beacon_proposer": 0, - "domain_beacon_attester": 1, - "domain_randao": 2, - "domain_deposit": 3, - "domain_voluntary_exit": 4, - "safe_slots_to_update_justified": 8, - "eth1_follow_distance": 1024, - "seconds_per_eth1_block": 14, - "boot_nodes": [], - "network_id": 1 -} -``` - -## `/spec/eth2_config` - -Requests the full `Eth2Config` object. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/spec/eth2_config` -Method | GET -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -{ - "spec_constants": "mainnet", - "spec": { - "genesis_slot": 0, - "base_rewards_per_epoch": 4, - "deposit_contract_tree_depth": 32, - "max_committees_per_slot": 64, - "target_committee_size": 128, - "min_per_epoch_churn_limit": 4, - "churn_limit_quotient": 65536, - "shuffle_round_count": 90, - "min_genesis_active_validator_count": 16384, - "min_genesis_time": 1578009600, - "min_deposit_amount": 1000000000, - "max_effective_balance": 32000000000, - "ejection_balance": 16000000000, - "effective_balance_increment": 1000000000, - "genesis_fork_version": "0x00000000", - "bls_withdrawal_prefix_byte": "0x00", - "genesis_delay": 172800, - "milliseconds_per_slot": 12000, - "min_attestation_inclusion_delay": 1, - "min_seed_lookahead": 1, - "max_seed_lookahead": 4, - "min_epochs_to_inactivity_penalty": 4, - "min_validator_withdrawability_delay": 256, - "shard_committee_period": 2048, - "base_reward_factor": 64, - "whistleblower_reward_quotient": 512, - "proposer_reward_quotient": 8, - "inactivity_penalty_quotient": 33554432, - "min_slashing_penalty_quotient": 32, - "domain_beacon_proposer": 0, - "domain_beacon_attester": 1, - "domain_randao": 2, - "domain_deposit": 3, - "domain_voluntary_exit": 4, - "safe_slots_to_update_justified": 8, - "eth1_follow_distance": 1024, - "seconds_per_eth1_block": 14, - "boot_nodes": [], - "network_id": 1 - } -} -``` - -## `/spec/slots_per_epoch` - -Requests the `SLOTS_PER_EPOCH` parameter from the specs that the node is running. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/spec/slots_per_epoch` -Method | GET -JSON Encoding | Number -Query Parameters | None -Typical Responses | 200 - -### Example Response - -```json -32 -``` \ No newline at end of file diff --git a/book/src/http/validator.md b/book/src/http/validator.md deleted file mode 100644 index eff0c6095..000000000 --- a/book/src/http/validator.md +++ /dev/null @@ -1,545 +0,0 @@ -# Lighthouse REST API: `/validator` - -The `/validator` endpoints provide the minimum functionality required for a validator -client to connect to the beacon node and produce blocks and attestations. - -## Endpoints - -HTTP Path | HTTP Method | Description | -| - | - | ---- | -[`/validator/duties`](#validatorduties) | POST | Provides block and attestation production information for validators. -[`/validator/subscribe`](#validatorsubscribe) | POST | Subscribes a list of validators to the beacon node for a particular duty/slot. -[`/validator/duties/all`](#validatordutiesall) | GET |Provides block and attestation production information for all validators. -[`/validator/duties/active`](#validatordutiesactive) | GET | Provides block and attestation production information for all active validators. -[`/validator/block`](#validatorblock-get) | GET | Retrieves the current beacon block for the validator to publish. -[`/validator/block`](#validatorblock-post) | POST | Publishes a signed block to the network. -[`/validator/attestation`](#validatorattestation) | GET | Retrieves the current best attestation for a validator to publish. -[`/validator/aggregate_attestation`](#validatoraggregate_attestation) | GET | Gets an aggregate attestation for validators to sign and publish. -[`/validator/attestations`](#validatorattestations) | POST | Publishes a list of raw unaggregated attestations to their appropriate subnets. -[`/validator/aggregate_and_proofs`](#validatoraggregate_and_proofs) | POST | Publishes a list of Signed aggregate and proofs for validators who are aggregators. - -## `/validator/duties` - -Request information about when a validator must produce blocks and attestations -at some given `epoch`. The information returned always refers to the canonical -chain and the same input parameters may yield different results after a re-org. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/duties` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Request Body - -Expects the following object in the POST request body: - -``` -{ - epoch: Epoch, - pubkeys: [PublicKey] -} -``` - -Duties are assigned on a per-epoch basis, all duties returned will contain -slots that are inside the given `epoch`. A set of duties will be returned for -each of the `pubkeys`. - -Validators who are not known to the beacon chain (e.g., have not yet deposited) -will have `null` values for most fields. - - -### Returns - -A set of duties for each given pubkey. - -### Example - -#### Request Body - -```json -{ - "epoch": 1203, - "pubkeys": [ - "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42" - ] -} -``` - -_Note: for demonstration purposes the second pubkey is some unknown pubkey._ - -#### Response Body - -```json -[ - { - "validator_pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "validator_index": 14935, - "attestation_slot": 38511, - "attestation_committee_index": 3, - "attestation_committee_position": 39, - "block_proposal_slots": [], - "aggregator_modulo": 5, - }, - { - "validator_pubkey": "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42", - "validator_index": null, - "attestation_slot": null, - "attestation_committee_index": null, - "attestation_committee_position": null, - "block_proposal_slots": [] - "aggregator_modulo": null, - } -] -``` - -## `/validator/duties/all` - -Returns the duties for all validators, equivalent to calling [Validator -Duties](#validator-duties) while providing all known validator public keys. - -Considering that duties for non-active validators will just be `null`, it is -generally more efficient to query using [Active Validator -Duties](#active-validator-duties). - -This endpoint will only return validators that were in the beacon state -in the given epoch. For example, if the query epoch is 10 and some validator -deposit was included in epoch 11, that validator will not be included in the -result. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/duties/all` -Method | GET -JSON Encoding | Object -Query Parameters | `epoch` -Typical Responses | 200 - -### Parameters - -The duties returned will all be inside the given `epoch` (`Epoch`) query -parameter. This parameter is required. - -### Returns - -The return format is identical to the [Validator Duties](#validator-duties) response body. - -## `/validator/duties/active` - -Returns the duties for all active validators, equivalent to calling [Validator -Duties](#validator-duties) while providing all known validator public keys that -are active in the given epoch. - -This endpoint will only return validators that were in the beacon state -in the given epoch. For example, if the query epoch is 10 and some validator -deposit was included in epoch 11, that validator will not be included in the -result. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/duties/active` -Method | GET -JSON Encoding | Object -Query Parameters | `epoch` -Typical Responses | 200 - -### Parameters - -The duties returned will all be inside the given `epoch` (`Epoch`) query -parameter. This parameter is required. - -### Returns - -The return format is identical to the [Validator Duties](#validator-duties) response body. - -## `/validator/subscribe` - -Posts a list of `ValidatorSubscription` to subscribe validators to -particular slots to perform attestation duties. - -This informs the beacon node to search for peers and subscribe to -required attestation subnets to perform the attestation duties required. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/subscribe` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Request Body - -Expects the following object in the POST request body: - -``` -[ - { - validator_index: 10, - attestation_committee_index: 12, - slot: 3, - is_aggregator: true - } -] -``` - -The `is_aggregator` informs the beacon node if the validator is an aggregator -for this slot/committee. - -### Returns - -A null object on success and an error indicating any failures. - -## `/validator/block` GET - - -Produces and returns an unsigned `BeaconBlock` object. - -The block will be produced with the given `slot` and the parent block will be the -highest block in the canonical chain that has a slot less than `slot`. The -block will still be produced if some other block is also known to be at `slot` -(i.e., it may produce a block that would be slashable if signed). - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/block` -Method | GET -JSON Encoding | Object -Query Parameters | `slot`, `randao_reveal` -Typical Responses | 200 - -### Parameters - - -- `slot` (`Slot`): The slot number for which the block is to be produced. -- `randao_reveal` (`Signature`): 96 bytes `Signature` for the randomness. - - -### Returns - -Returns a `BeaconBlock` object. - -#### Response Body - -```json -{ - "slot": 33, - "parent_root": "0xf54de54bd33e33aee4706cffff4bd991bcbf522f2551ab007180479c63f4fe912", - "state_root": "0x615c887bad27bc05754d627d941e1730e1b4c77b2eb4378c195ac8a8203bbf26", - "body": { - "randao_reveal": "0x8d7b2a32b026e9c79aae6ec6b83eabae89d60cacd65ac41ed7d2f4be9dd8c89c1bf7cd3d700374e18d03d12f6a054c23006f64f0e4e8b7cf37d6ac9a4c7d815c858120c54673b7d3cb2bb1550a4d659eaf46e34515677c678b70d6f62dbf89f", - "eth1_data": { - "deposit_root": "0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", - "deposit_count": 8, - "block_hash": "0x2b32db6c2c0a6235fb1397e8225ea85e0f0e6e8c7b126d0016ccbde0e667151e" - }, - "graffiti": "0x736967702f6c69676874686f7573652d302e312e312d7076572656c65617365", - "proposer_slashings": [], - "attester_slashings": [], - "attestations": [], - "deposits": [], - "voluntary_exits": [] - } -} -``` - -## `/validator/block` POST - -Accepts a `SignedBeaconBlock` for verification. If it is valid, it will be -imported into the local database and published on the network. Invalid blocks -will not be published to the network. - -A block may be considered invalid because it is fundamentally incorrect, or its -parent has not yet been imported. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/block` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200/202 - - -### Request Body - -Expects a JSON encoded `SignedBeaconBlock` in the POST request body: - -### Returns - -Returns a null object if the block passed all block validation and is published to the network. -Else, returns a processing error description. - -### Example - -### Request Body - -```json -{ - "message": { - "slot": 33, - "parent_root": "0xf54de54bd33e33aee4706cffff4bd991bcbf522f2551ab007180479c63f4fe912", - "state_root": "0x615c887bad27bc05754d627d941e1730e1b4c77b2eb4378c195ac8a8203bbf26", - "body": { - "randao_reveal": "0x8d7b2a32b026e9c79aae6ec6b83eabae89d60cacd65ac41ed7d2f4be9dd8c89c1bf7cd3d700374e18d03d12f6a054c23006f64f0e4e8b7cf37d6ac9a4c7d815c858120c54673b7d3cb2bb1550a4d659eaf46e34515677c678b70d6f62dbf89f", - "eth1_data": { - "deposit_root": "0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", - "deposit_count": 8, - "block_hash": "0x2b32db6c2c0a6235fb1397e8225ea85e0f0e6e8c7b126d0016ccbde0e667151e" - }, - "graffiti": "0x736967702f6c69676874686f7573652d302e312e312d7076572656c65617365", - "proposer_slashings": [ - - ], - "attester_slashings": [ - - ], - "attestations": [ - - ], - "deposits": [ - - ], - "voluntary_exits": [ - - ] - } - }, - "signature": "0x965ced900dbabd0a78b81a0abb5d03407be0d38762104316416347f2ea6f82652b5759396f402e85df8ee18ba2c60145037c73b1c335f4272f1751a1cd89862b7b4937c035e350d0108554bd4a8930437ec3311c801a65fe8e5ba022689b5c24" -} -``` - -## `/validator/attestation` - -Produces and returns an unsigned `Attestation` from the current state. - -The attestation will reference the `beacon_block_root` of the highest block in -the canonical chain with a slot equal to or less than the given `slot`. - -An error will be returned if the given slot is more than -`SLOTS_PER_HISTORICAL_VECTOR` slots behind the current head block. - -This endpoint is not protected against slashing. Signing the returned -attestation may result in a slashable offence. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/attestation` -Method | GET -JSON Encoding | Object -Query Parameters | `slot`, `committee_index` -Typical Responses | 200 - -### Parameters - - -- `slot` (`Slot`): The slot number for which the attestation is to be produced. -- `committee_index` (`CommitteeIndex`): The index of the committee that makes the attestation. - - -### Returns - -Returns a `Attestation` object with a default signature. The `signature` field should be replaced by the valid signature. - -#### Response Body - -```json -{ - "aggregation_bits": "0x01", - "data": { - "slot": 100, - "index": 0, - "beacon_block_root": "0xf22e4ec281136d119eabcd4d9d248aeacd042eb63d8d7642f73ad3e71f1c9283", - "source": { - "epoch": 2, - "root": "0x34c1244535c923f08e7f83170d41a076e4f1ec61013846b3a615a1d109d3c329" - }, - "target": { - "epoch": 3, - "root": "0xaefd23b384994dc0c1a6b77836bdb2f24f209ebfe6c4819324d9685f4a43b4e1" - } - }, - "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" -} -``` - - - -## `/validator/aggregate_attestation` - -Requests an `AggregateAttestation` from the beacon node that has a -specific `attestation.data`. If no aggregate attestation is known this will -return a null object. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/aggregate_attestation` -Method | GET -JSON Encoding | Object -Query Parameters | `attestation_data` -Typical Responses | 200 - -### Returns - -Returns a null object if the attestation data passed is not known to the beacon -node. - -### Example - -### Request Body - -```json -{ - "aggregation_bits": "0x03", - "data": { - "slot": 3, - "index": 0, - "beacon_block_root": "0x0b6a1f7a9baa38d00ef079ba861b7587662565ca2502fb9901741c1feb8bb3c9", - "source": { - "epoch": 0, - "root": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - "target": { - "epoch": 0, - "root": "0xad2c360ab8c8523db278a7d7ced22f3810800f2fdc282defb6db216689d376bd" - } - }, - "signature": "0xb76a1768c18615b5ade91a92e7d2ed0294f7e088e56e30fbe7e3aa6799c443b11bccadd578ca2cbd95d395ab689b9e4d03c88a56641791ab38dfa95dc1f4d24d1b19b9d36c96c20147ad03$649bd3c6c7e8a39cf2ffb99e07b4964d52854559f" -} -``` - - -## `/validator/attestations` - -Accepts a list of `Attestation` for verification. If they are valid, they will be imported -into the local database and published to the network. Invalid attestations will -not be published to the network. - -An attestation may be considered invalid because it is fundamentally incorrect -or because the beacon node has not imported the relevant blocks required to -verify it. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/attestations` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200/202 - - -### Request Body - -Expects a JSON encoded list of signed `Attestation` objects in the POST request body. In -accordance with the naive aggregation scheme, the attestation _must_ have -exactly one of the `attestation.aggregation_bits` fields set. - -### Returns - -Returns a null object if the attestation passed all validation and is published to the network. -Else, returns a processing error description. - -### Example - -### Request Body - -```json -{ - "aggregation_bits": "0x03", - "data": { - "slot": 3, - "index": 0, - "beacon_block_root": "0x0b6a1f7a9baa38d00ef079ba861b7587662565ca2502fb9901741c1feb8bb3c9", - "source": { - "epoch": 0, - "root": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - "target": { - "epoch": 0, - "root": "0xad2c360ab8c8523db278a7d7ced22f3810800f2fdc282defb6db216689d376bd" - } - }, - "signature": "0xb76a1768c18615b5ade91a92e7d2ed0294f7e088e56e30fbe7e3aa6799c443b11bccadd578ca2cbd95d395ab689b9e4d03c88a56641791ab38dfa95dc1f4d24d1b19b9d36c96c20147ad03$649bd3c6c7e8a39cf2ffb99e07b4964d52854559f" -} -``` - -## `/validator/aggregate_and_proofs` - -Accepts a list of `SignedAggregateAndProof` for publication. If they are valid -(the validator is an aggregator and the signatures can be verified) these -are published to the network on the global aggregate gossip topic. - -### HTTP Specification - -| Property | Specification | -| --- |--- | -Path | `/validator/aggregate_and_proofs` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200/202 - -### Request Body - -Expects a JSON encoded list of `SignedAggregateAndProof` objects in the POST request body. - -### Returns - -Returns a null object if the attestation passed all validation and is published to the network. -Else, returns a processing error description. - -### Example - -### Request Body - -```json -[ - { - "message": { - "aggregator_index": 12, - "aggregate": { - "aggregation_bits": "0x03", - "data": { - "slot": 3, - "index": 0, - "beacon_block_root": "0x0b6a1f7a9baa38d00ef079ba861b7587662565ca2502fb9901741c1feb8bb3c9", - "source": { - "epoch": 0, - "root": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - "target": { - "epoch": 0, - "root": "0xad2c360ab8c8523db278a7d7ced22f3810800f2fdc282defb6db216689d376bd" - } - }, - "signature": "0xb76a1768c18615b5ade91a92e7d2ed0294f7e088e56e30fbe7e3aa6799c443b11bccadd578ca2cbd95d395ab689b9e4d03c88a56641791ab38dfa95dc1f4d24d1b19b9d36c96c20147ad03649bd3c6c7e8a39cf2ffb99e07b4964d52854559f" - }, - "selection_proof": "0xb76a1768c18615b5ade91a92e7d2ed0294f7e088e56e30fbe7e3aa6799c443b11bccadd578ca2cbd95d395ab689b9e4d03c88a56641791ab38dfa95dc1f4d24d1b19b9d36c96c20147ad03649bd3c6c7e8a39cf2ffb99e07b4964d52854559f" - } - signature: "0xb76a1768c18615b5ade91a92e7d2ed0294f7e088e56e30fbe7e3aa6799c443b11bccadd578ca2cbd95d395ab689b9e4d03c88a56641791ab38dfa95dc1f4d24d1b19b9d36c96c20147ad03649bd3c6c7e8a39cf2ffb99e07b4964d52854559f" - } -] -``` -_Note: The data in this request is for demonstrating types and does not -contain real data_ diff --git a/book/src/http/consensus.md b/book/src/validator-inclusion.md similarity index 52% rename from book/src/http/consensus.md rename to book/src/validator-inclusion.md index c71b78ce3..ce8e61caf 100644 --- a/book/src/http/consensus.md +++ b/book/src/validator-inclusion.md @@ -1,16 +1,21 @@ -# Lighthouse REST API: `/consensus` +# Validator Inclusion APIs -The `/consensus` endpoints provide information on results of the proof-of-stake -voting process used for finality/justification under Casper FFG. +The `/lighthouse/validator_inclusion` API endpoints provide information on +results of the proof-of-stake voting process used for finality/justification +under Casper FFG. + +These endpoints are not stable or included in the Eth2 standard API. As such, +they are subject to change or removal without a change in major release +version. ## Endpoints HTTP Path | Description | | --- | -- | -[`/consensus/global_votes`](#consensusglobal_votes) | A global vote count for a given epoch. -[`/consensus/individual_votes`](#consensusindividual_votes) | A per-validator breakdown of votes in a given epoch. +[`/lighthouse/validator_inclusion/{epoch}/global`](#global) | A global vote count for a given epoch. +[`/lighthouse/validator_inclusion/{epoch}/{validator_id}`](#individual) | A per-validator breakdown of votes in a given epoch. -## `/consensus/global_votes` +## Global Returns a global count of votes for some given `epoch`. The results are included both for the current and previous (`epoch - 1`) epochs since both are required @@ -75,40 +80,27 @@ voting upon the previous epoch included in a block. When this value is greater than or equal to `2/3` it is possible that the beacon chain may justify and/or finalize the epoch. -### HTTP Specification +### HTTP Example -| Property | Specification | -| --- |--- | -Path | `/consensus/global_votes` -Method | GET -JSON Encoding | Object -Query Parameters | `epoch` -Typical Responses | 200 - -### Parameters - -Requires the `epoch` (`Epoch`) query parameter to determine which epoch will be -considered the current epoch. - -### Returns - -A report on global validator voting participation. - -### Example +```bash +curl -X GET "http://localhost:5052/lighthouse/validator_inclusion/0/global" -H "accept: application/json" | jq +``` ```json { - "current_epoch_active_gwei": 52377600000000, - "previous_epoch_active_gwei": 52377600000000, - "current_epoch_attesting_gwei": 50740900000000, - "current_epoch_target_attesting_gwei": 49526000000000, - "previous_epoch_attesting_gwei": 52377600000000, - "previous_epoch_target_attesting_gwei": 51063400000000, - "previous_epoch_head_attesting_gwei": 9248600000000 + "data": { + "current_epoch_active_gwei": 642688000000000, + "previous_epoch_active_gwei": 642688000000000, + "current_epoch_attesting_gwei": 366208000000000, + "current_epoch_target_attesting_gwei": 366208000000000, + "previous_epoch_attesting_gwei": 1000000000, + "previous_epoch_target_attesting_gwei": 1000000000, + "previous_epoch_head_attesting_gwei": 1000000000 + } } ``` -## `/consensus/individual_votes` +## Individual Returns a per-validator summary of how that validator performed during the current epoch. @@ -117,73 +109,26 @@ The [Global Votes](#consensusglobal_votes) endpoint is the summation of all of t individual values, please see it for definitions of terms like "current_epoch", "previous_epoch" and "target_attester". -### HTTP Specification -| Property | Specification | -| --- |--- | -Path | `/consensus/individual_votes` -Method | POST -JSON Encoding | Object -Query Parameters | None -Typical Responses | 200 - -### Request Body - -Expects the following object in the POST request body: +### HTTP Example +```bash +curl -X GET "http://localhost:5052/lighthouse/validator_inclusion/0/42" -H "accept: application/json" | jq ``` -{ - epoch: Epoch, - pubkeys: [PublicKey] -} -``` - -### Returns - -A report on the validators voting participation. - -### Example - -#### Request Body ```json { - "epoch": 1203, - "pubkeys": [ - "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42" - ] + "data": { + "is_slashed": false, + "is_withdrawable_in_current_epoch": false, + "is_active_in_current_epoch": true, + "is_active_in_previous_epoch": true, + "current_epoch_effective_balance_gwei": 32000000000, + "is_current_epoch_attester": false, + "is_current_epoch_target_attester": false, + "is_previous_epoch_attester": false, + "is_previous_epoch_target_attester": false, + "is_previous_epoch_head_attester": false + } } ``` - -_Note: for demonstration purposes the second pubkey is some unknown pubkey._ - -#### Response Body - -```json -[ - { - "epoch": 1203, - "pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", - "validator_index": 14935, - "vote": { - "is_slashed": false, - "is_withdrawable_in_current_epoch": false, - "is_active_in_current_epoch": true, - "is_active_in_previous_epoch": true, - "current_epoch_effective_balance_gwei": 3200000000, - "is_current_epoch_attester": true, - "is_current_epoch_target_attester": true, - "is_previous_epoch_attester": true, - "is_previous_epoch_target_attester": true, - "is_previous_epoch_head_attester": false - } - }, - { - "epoch": 1203, - "pubkey": "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42", - "validator_index": null, - "vote": null - } -] -``` diff --git a/book/src/websockets.md b/book/src/websockets.md deleted file mode 100644 index 69cf0e18d..000000000 --- a/book/src/websockets.md +++ /dev/null @@ -1,111 +0,0 @@ -# Websocket API - -**Note: the WebSocket server _only_ emits events. It does not accept any -requests. Use the [HTTP API](./http.md) for requests.** - -By default, a Lighthouse `beacon_node` exposes a websocket server on `localhost:5053`. - -The following CLI flags control the websocket server: - -- `--no-ws`: disable the websocket server. -- `--ws-port`: specify the listen port of the server. -- `--ws-address`: specify the listen address of the server. - -All clients connected to the websocket server will receive the same stream of events, all triggered -by the `BeaconChain`. Each event is a JSON object with the following schema: - -```json -{ - "event": "string", - "data": "object" -} -``` - -## Events - -The following events may be emitted: - -### Beacon Head Changed - -Occurs whenever the canonical head of the beacon chain changes. - -```json -{ - "event": "beacon_head_changed", - "data": { - "reorg": "boolean", - "current_head_beacon_block_root": "string", - "previous_head_beacon_block_root": "string" - } -} -``` - -### Beacon Finalization - -Occurs whenever the finalized checkpoint of the canonical head changes. - -```json -{ - "event": "beacon_finalization", - "data": { - "epoch": "number", - "root": "string" - } -} -``` - -### Beacon Block Imported - -Occurs whenever the beacon node imports a valid block. - -```json -{ - "event": "beacon_block_imported", - "data": { - "block": "object" - } -} -``` - -### Beacon Block Rejected - -Occurs whenever the beacon node rejects a block because it is invalid or an -error occurred during validation. - -```json -{ - "event": "beacon_block_rejected", - "data": { - "reason": "string", - "block": "object" - } -} -``` - -### Beacon Attestation Imported - -Occurs whenever the beacon node imports a valid attestation. - -```json -{ - "event": "beacon_attestation_imported", - "data": { - "attestation": "object" - } -} -``` - -### Beacon Attestation Rejected - -Occurs whenever the beacon node rejects an attestation because it is invalid or -an error occurred during validation. - -```json -{ - "event": "beacon_attestation_rejected", - "data": { - "reason": "string", - "attestation": "object" - } -} -``` diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml new file mode 100644 index 000000000..f7ccfcf34 --- /dev/null +++ b/common/eth2/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "eth2" +version = "0.1.0" +authors = ["Paul Hauner <paul@paulhauner.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.52" +types = { path = "../../consensus/types" } +hex = "0.4.2" +reqwest = { version = "0.10.8", features = ["json"] } +eth2_libp2p = { path = "../../beacon_node/eth2_libp2p" } +proto_array = { path = "../../consensus/proto_array", optional = true } +serde_utils = { path = "../../consensus/serde_utils" } + +[target.'cfg(target_os = "linux")'.dependencies] +psutil = { version = "3.1.0", optional = true } +procinfo = { version = "0.4.2", optional = true } + +[features] +default = ["lighthouse"] +lighthouse = ["proto_array", "psutil", "procinfo"] diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs new file mode 100644 index 000000000..b0fbc2566 --- /dev/null +++ b/common/eth2/src/lib.rs @@ -0,0 +1,784 @@ +//! This crate provides two major things: +//! +//! 1. The types served by the `http_api` crate. +//! 2. A wrapper around `reqwest` that forms a HTTP client, able of consuming the endpoints served +//! by the `http_api` crate. +//! +//! Eventually it would be ideal to publish this crate on crates.io, however we have some local +//! dependencies preventing this presently. + +#[cfg(feature = "lighthouse")] +pub mod lighthouse; +pub mod types; + +use self::types::*; +use reqwest::{IntoUrl, Response}; +use serde::{de::DeserializeOwned, Serialize}; +use std::convert::TryFrom; +use std::fmt; + +pub use reqwest; +pub use reqwest::{StatusCode, Url}; + +#[derive(Debug)] +pub enum Error { + /// The `reqwest` client raised an error. + Reqwest(reqwest::Error), + /// The server returned an error message where the body was able to be parsed. + ServerMessage(ErrorMessage), + /// The server returned an error message where the body was unable to be parsed. + StatusCode(StatusCode), + /// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`. + InvalidUrl(Url), +} + +impl Error { + /// If the error has a HTTP status code, return it. + pub fn status(&self) -> Option<StatusCode> { + match self { + Error::Reqwest(error) => error.status(), + Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), + Error::StatusCode(status) => Some(*status), + Error::InvalidUrl(_) => None, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a +/// Lighthouse Beacon Node HTTP server (`http_api`). +#[derive(Clone)] +pub struct BeaconNodeHttpClient { + client: reqwest::Client, + server: Url, +} + +impl BeaconNodeHttpClient { + pub fn new(server: Url) -> Self { + Self { + client: reqwest::Client::new(), + server, + } + } + + pub fn from_components(server: Url, client: reqwest::Client) -> Self { + Self { client, server } + } + + /// Return the path with the standard `/eth1/v1` prefix applied. + fn eth_path(&self) -> Result<Url, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v1"); + + Ok(path) + } + + /// Perform a HTTP GET request. + async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> { + let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; + ok_or_error(response) + .await? + .json() + .await + .map_err(Error::Reqwest) + } + + /// Perform a HTTP GET request, returning `None` on a 404 error. + async fn get_opt<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<Option<T>, Error> { + let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; + match ok_or_error(response).await { + Ok(resp) => resp.json().await.map(Option::Some).map_err(Error::Reqwest), + Err(err) => { + if err.status() == Some(StatusCode::NOT_FOUND) { + Ok(None) + } else { + Err(err) + } + } + } + } + + /// Perform a HTTP POST request. + async fn post<T: Serialize, U: IntoUrl>(&self, url: U, body: &T) -> Result<(), Error> { + let response = self + .client + .post(url) + .json(body) + .send() + .await + .map_err(Error::Reqwest)?; + ok_or_error(response).await?; + Ok(()) + } + + /// `GET beacon/genesis` + /// + /// ## Errors + /// + /// May return a `404` if beacon chain genesis has not yet occurred. + pub async fn get_beacon_genesis(&self) -> Result<GenericResponse<GenesisData>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("genesis"); + + self.get(path).await + } + + /// `GET beacon/states/{state_id}/root` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_root( + &self, + state_id: StateId, + ) -> Result<Option<GenericResponse<RootData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("root"); + + self.get_opt(path).await + } + + /// `GET beacon/states/{state_id}/fork` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_fork( + &self, + state_id: StateId, + ) -> Result<Option<GenericResponse<Fork>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("fork"); + + self.get_opt(path).await + } + + /// `GET beacon/states/{state_id}/finality_checkpoints` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_finality_checkpoints( + &self, + state_id: StateId, + ) -> Result<Option<GenericResponse<FinalityCheckpointsData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("finality_checkpoints"); + + self.get_opt(path).await + } + + /// `GET beacon/states/{state_id}/validators` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_validators( + &self, + state_id: StateId, + ) -> Result<Option<GenericResponse<Vec<ValidatorData>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("validators"); + + self.get_opt(path).await + } + + /// `GET beacon/states/{state_id}/committees?slot,index` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_committees( + &self, + state_id: StateId, + epoch: Epoch, + slot: Option<Slot>, + index: Option<u64>, + ) -> Result<Option<GenericResponse<Vec<CommitteeData>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("committees") + .push(&epoch.to_string()); + + if let Some(slot) = slot { + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()); + } + + if let Some(index) = index { + path.query_pairs_mut() + .append_pair("index", &index.to_string()); + } + + self.get_opt(path).await + } + + /// `GET beacon/states/{state_id}/validators/{validator_id}` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_validator_id( + &self, + state_id: StateId, + validator_id: &ValidatorId, + ) -> Result<Option<GenericResponse<ValidatorData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("validators") + .push(&validator_id.to_string()); + + self.get_opt(path).await + } + + /// `GET beacon/headers?slot,parent_root` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_headers( + &self, + slot: Option<Slot>, + parent_root: Option<Hash256>, + ) -> Result<Option<GenericResponse<Vec<BlockHeaderData>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("headers"); + + if let Some(slot) = slot { + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()); + } + + if let Some(root) = parent_root { + path.query_pairs_mut() + .append_pair("parent_root", &format!("{:?}", root)); + } + + self.get_opt(path).await + } + + /// `GET beacon/headers/{block_id}` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_headers_block_id( + &self, + block_id: BlockId, + ) -> Result<Option<GenericResponse<BlockHeaderData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("headers") + .push(&block_id.to_string()); + + self.get_opt(path).await + } + + /// `POST beacon/blocks` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn post_beacon_blocks<T: EthSpec>( + &self, + block: &SignedBeaconBlock<T>, + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blocks"); + + self.post(path, block).await?; + + Ok(()) + } + + /// `GET beacon/blocks` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_blocks<T: EthSpec>( + &self, + block_id: BlockId, + ) -> Result<Option<GenericResponse<SignedBeaconBlock<T>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blocks") + .push(&block_id.to_string()); + + self.get_opt(path).await + } + + /// `GET beacon/blocks/{block_id}/root` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_blocks_root( + &self, + block_id: BlockId, + ) -> Result<Option<GenericResponse<RootData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blocks") + .push(&block_id.to_string()) + .push("root"); + + self.get_opt(path).await + } + + /// `GET beacon/blocks/{block_id}/attestations` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_blocks_attestations<T: EthSpec>( + &self, + block_id: BlockId, + ) -> Result<Option<GenericResponse<Vec<Attestation<T>>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blocks") + .push(&block_id.to_string()) + .push("attestations"); + + self.get_opt(path).await + } + + /// `POST beacon/pool/attestations` + pub async fn post_beacon_pool_attestations<T: EthSpec>( + &self, + attestation: &Attestation<T>, + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("attestations"); + + self.post(path, attestation).await?; + + Ok(()) + } + + /// `GET beacon/pool/attestations` + pub async fn get_beacon_pool_attestations<T: EthSpec>( + &self, + ) -> Result<GenericResponse<Vec<Attestation<T>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("attestations"); + + self.get(path).await + } + + /// `POST beacon/pool/attester_slashings` + pub async fn post_beacon_pool_attester_slashings<T: EthSpec>( + &self, + slashing: &AttesterSlashing<T>, + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("attester_slashings"); + + self.post(path, slashing).await?; + + Ok(()) + } + + /// `GET beacon/pool/attester_slashings` + pub async fn get_beacon_pool_attester_slashings<T: EthSpec>( + &self, + ) -> Result<GenericResponse<Vec<AttesterSlashing<T>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("attester_slashings"); + + self.get(path).await + } + + /// `POST beacon/pool/proposer_slashings` + pub async fn post_beacon_pool_proposer_slashings( + &self, + slashing: &ProposerSlashing, + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("proposer_slashings"); + + self.post(path, slashing).await?; + + Ok(()) + } + + /// `GET beacon/pool/proposer_slashings` + pub async fn get_beacon_pool_proposer_slashings( + &self, + ) -> Result<GenericResponse<Vec<ProposerSlashing>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("proposer_slashings"); + + self.get(path).await + } + + /// `POST beacon/pool/voluntary_exits` + pub async fn post_beacon_pool_voluntary_exits( + &self, + exit: &SignedVoluntaryExit, + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("voluntary_exits"); + + self.post(path, exit).await?; + + Ok(()) + } + + /// `GET beacon/pool/voluntary_exits` + pub async fn get_beacon_pool_voluntary_exits( + &self, + ) -> Result<GenericResponse<Vec<SignedVoluntaryExit>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("voluntary_exits"); + + self.get(path).await + } + + /// `GET config/fork_schedule` + pub async fn get_config_fork_schedule(&self) -> Result<GenericResponse<Vec<Fork>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("config") + .push("fork_schedule"); + + self.get(path).await + } + + /// `GET config/fork_schedule` + pub async fn get_config_spec(&self) -> Result<GenericResponse<YamlConfig>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("config") + .push("spec"); + + self.get(path).await + } + + /// `GET config/deposit_contract` + pub async fn get_config_deposit_contract( + &self, + ) -> Result<GenericResponse<DepositContractData>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("config") + .push("deposit_contract"); + + self.get(path).await + } + + /// `GET node/version` + pub async fn get_node_version(&self) -> Result<GenericResponse<VersionData>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("node") + .push("version"); + + self.get(path).await + } + + /// `GET node/syncing` + pub async fn get_node_syncing(&self) -> Result<GenericResponse<SyncingData>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("node") + .push("syncing"); + + self.get(path).await + } + + /// `GET debug/beacon/states/{state_id}` + pub async fn get_debug_beacon_states<T: EthSpec>( + &self, + state_id: StateId, + ) -> Result<Option<GenericResponse<BeaconState<T>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("debug") + .push("beacon") + .push("states") + .push(&state_id.to_string()); + + self.get_opt(path).await + } + + /// `GET debug/beacon/heads` + pub async fn get_debug_beacon_heads( + &self, + ) -> Result<GenericResponse<Vec<ChainHeadData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("debug") + .push("beacon") + .push("heads"); + + self.get(path).await + } + + /// `GET validator/duties/attester/{epoch}?index` + /// + /// ## Note + /// + /// The `index` query parameter accepts a list of validator indices. + pub async fn get_validator_duties_attester( + &self, + epoch: Epoch, + index: Option<&[u64]>, + ) -> Result<GenericResponse<Vec<AttesterData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("duties") + .push("attester") + .push(&epoch.to_string()); + + if let Some(index) = index { + let string = index + .iter() + .map(|i| i.to_string()) + .collect::<Vec<_>>() + .join(","); + path.query_pairs_mut().append_pair("index", &string); + } + + self.get(path).await + } + + /// `GET validator/duties/proposer/{epoch}` + pub async fn get_validator_duties_proposer( + &self, + epoch: Epoch, + ) -> Result<GenericResponse<Vec<ProposerData>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("duties") + .push("proposer") + .push(&epoch.to_string()); + + self.get(path).await + } + + /// `GET validator/duties/attester/{epoch}?index` + /// + /// ## Note + /// + /// The `index` query parameter accepts a list of validator indices. + pub async fn get_validator_blocks<T: EthSpec>( + &self, + slot: Slot, + randao_reveal: SignatureBytes, + graffiti: Option<&Graffiti>, + ) -> Result<GenericResponse<BeaconBlock<T>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("blocks") + .push(&slot.to_string()); + + path.query_pairs_mut() + .append_pair("randao_reveal", &randao_reveal.to_string()); + + if let Some(graffiti) = graffiti { + path.query_pairs_mut() + .append_pair("graffiti", &graffiti.to_string()); + } + + self.get(path).await + } + + /// `GET validator/attestation_data?slot,committee_index` + pub async fn get_validator_attestation_data( + &self, + slot: Slot, + committee_index: CommitteeIndex, + ) -> Result<GenericResponse<AttestationData>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("attestation_data"); + + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()) + .append_pair("committee_index", &committee_index.to_string()); + + self.get(path).await + } + + /// `GET validator/attestation_attestation?slot,attestation_data_root` + pub async fn get_validator_aggregate_attestation<T: EthSpec>( + &self, + slot: Slot, + attestation_data_root: Hash256, + ) -> Result<Option<GenericResponse<Attestation<T>>>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("aggregate_attestation"); + + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()) + .append_pair( + "attestation_data_root", + &format!("{:?}", attestation_data_root), + ); + + self.get_opt(path).await + } + + /// `POST validator/aggregate_and_proofs` + pub async fn post_validator_aggregate_and_proof<T: EthSpec>( + &self, + aggregate: &SignedAggregateAndProof<T>, + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("aggregate_and_proofs"); + + self.post(path, aggregate).await?; + + Ok(()) + } + + /// `POST validator/beacon_committee_subscriptions` + pub async fn post_validator_beacon_committee_subscriptions( + &self, + subscriptions: &[BeaconCommitteeSubscription], + ) -> Result<(), Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("beacon_committee_subscriptions"); + + self.post(path, &subscriptions).await?; + + Ok(()) + } +} + +/// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an +/// appropriate error message. +async fn ok_or_error(response: Response) -> Result<Response, Error> { + let status = response.status(); + + if status == StatusCode::OK { + Ok(response) + } else if let Ok(message) = response.json().await { + Err(Error::ServerMessage(message)) + } else { + Err(Error::StatusCode(status)) + } +} diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs new file mode 100644 index 000000000..8bfbad84e --- /dev/null +++ b/common/eth2/src/lighthouse.rs @@ -0,0 +1,224 @@ +//! This module contains endpoints that are non-standard and only available on Lighthouse servers. + +use crate::{ + types::{Epoch, EthSpec, GenericResponse, ValidatorId}, + BeaconNodeHttpClient, Error, +}; +use proto_array::core::ProtoArray; +use serde::{Deserialize, Serialize}; + +pub use eth2_libp2p::{types::SyncState, PeerInfo}; + +/// Information returned by `peers` and `connected_peers`. +// TODO: this should be deserializable.. +#[derive(Debug, Clone, Serialize)] +#[serde(bound = "T: EthSpec")] +pub struct Peer<T: EthSpec> { + /// The Peer's ID + pub peer_id: String, + /// The PeerInfo associated with the peer. + pub peer_info: PeerInfo<T>, +} + +/// The results of validators voting during an epoch. +/// +/// Provides information about the current and previous epochs. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GlobalValidatorInclusionData { + /// The total effective balance of all active validators during the _current_ epoch. + pub current_epoch_active_gwei: u64, + /// The total effective balance of all active validators during the _previous_ epoch. + pub previous_epoch_active_gwei: u64, + /// The total effective balance of all validators who attested during the _current_ epoch. + pub current_epoch_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _current_ epoch and + /// agreed with the state about the beacon block at the first slot of the _current_ epoch. + pub current_epoch_target_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _previous_ epoch. + pub previous_epoch_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _previous_ epoch and + /// agreed with the state about the beacon block at the first slot of the _previous_ epoch. + pub previous_epoch_target_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _previous_ epoch and + /// agreed with the state about the beacon block at the time of attestation. + pub previous_epoch_head_attesting_gwei: u64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorInclusionData { + /// True if the validator has been slashed, ever. + pub is_slashed: bool, + /// True if the validator can withdraw in the current epoch. + pub is_withdrawable_in_current_epoch: bool, + /// True if the validator was active in the state's _current_ epoch. + pub is_active_in_current_epoch: bool, + /// True if the validator was active in the state's _previous_ epoch. + pub is_active_in_previous_epoch: bool, + /// The validator's effective balance in the _current_ epoch. + pub current_epoch_effective_balance_gwei: u64, + /// True if the validator had an attestation included in the _current_ epoch. + pub is_current_epoch_attester: bool, + /// True if the validator's beacon block root attestation for the first slot of the _current_ + /// epoch matches the block root known to the state. + pub is_current_epoch_target_attester: bool, + /// True if the validator had an attestation included in the _previous_ epoch. + pub is_previous_epoch_attester: bool, + /// True if the validator's beacon block root attestation for the first slot of the _previous_ + /// epoch matches the block root known to the state. + pub is_previous_epoch_target_attester: bool, + /// True if the validator's beacon block root attestation in the _previous_ epoch at the + /// attestation's slot (`attestation_data.slot`) matches the block root known to the state. + pub is_previous_epoch_head_attester: bool, +} + +#[cfg(target_os = "linux")] +use {procinfo::pid, psutil::process::Process}; + +/// Reports on the health of the Lighthouse instance. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Health { + /// The pid of this process. + pub pid: u32, + /// The number of threads used by this pid. + pub pid_num_threads: i32, + /// The total resident memory used by this pid. + pub pid_mem_resident_set_size: u64, + /// The total virtual memory used by this pid. + pub pid_mem_virtual_memory_size: u64, + /// Total virtual memory on the system + pub sys_virt_mem_total: u64, + /// Total virtual memory available for new processes. + pub sys_virt_mem_available: u64, + /// Total virtual memory used on the system + pub sys_virt_mem_used: u64, + /// Total virtual memory not used on the system + pub sys_virt_mem_free: u64, + /// Percentage of virtual memory used on the system + pub sys_virt_mem_percent: f32, + /// System load average over 1 minute. + pub sys_loadavg_1: f64, + /// System load average over 5 minutes. + pub sys_loadavg_5: f64, + /// System load average over 15 minutes. + pub sys_loadavg_15: f64, +} + +impl Health { + #[cfg(not(target_os = "linux"))] + pub fn observe() -> Result<Self, String> { + Err("Health is only available on Linux".into()) + } + + #[cfg(target_os = "linux")] + pub fn observe() -> Result<Self, String> { + let process = + Process::current().map_err(|e| format!("Unable to get current process: {:?}", e))?; + + let process_mem = process + .memory_info() + .map_err(|e| format!("Unable to get process memory info: {:?}", e))?; + + let stat = pid::stat_self().map_err(|e| format!("Unable to get stat: {:?}", e))?; + + let vm = psutil::memory::virtual_memory() + .map_err(|e| format!("Unable to get virtual memory: {:?}", e))?; + let loadavg = + psutil::host::loadavg().map_err(|e| format!("Unable to get loadavg: {:?}", e))?; + + Ok(Self { + pid: process.pid(), + pid_num_threads: stat.num_threads, + pid_mem_resident_set_size: process_mem.rss(), + pid_mem_virtual_memory_size: process_mem.vms(), + sys_virt_mem_total: vm.total(), + sys_virt_mem_available: vm.available(), + sys_virt_mem_used: vm.used(), + sys_virt_mem_free: vm.free(), + sys_virt_mem_percent: vm.percent(), + sys_loadavg_1: loadavg.one, + sys_loadavg_5: loadavg.five, + sys_loadavg_15: loadavg.fifteen, + }) + } +} + +impl BeaconNodeHttpClient { + /// `GET lighthouse/health` + pub async fn get_lighthouse_health(&self) -> Result<GenericResponse<Health>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("health"); + + self.get(path).await + } + + /// `GET lighthouse/syncing` + pub async fn get_lighthouse_syncing(&self) -> Result<GenericResponse<SyncState>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("syncing"); + + self.get(path).await + } + + /* + * Note: + * + * The `lighthouse/peers` endpoints do not have functions here. We are yet to implement + * `Deserialize` on the `PeerInfo` struct since it contains use of `Instant`. This could be + * fairly simply achieved, if desired. + */ + + /// `GET lighthouse/proto_array` + pub async fn get_lighthouse_proto_array(&self) -> Result<GenericResponse<ProtoArray>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("proto_array"); + + self.get(path).await + } + + /// `GET lighthouse/validator_inclusion/{epoch}/global` + pub async fn get_lighthouse_validator_inclusion_global( + &self, + epoch: Epoch, + ) -> Result<GenericResponse<GlobalValidatorInclusionData>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validator_inclusion") + .push(&epoch.to_string()) + .push("global"); + + self.get(path).await + } + + /// `GET lighthouse/validator_inclusion/{epoch}/{validator_id}` + pub async fn get_lighthouse_validator_inclusion( + &self, + epoch: Epoch, + validator_id: ValidatorId, + ) -> Result<GenericResponse<Option<ValidatorInclusionData>>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validator_inclusion") + .push(&epoch.to_string()) + .push(&validator_id.to_string()); + + self.get(path).await + } +} diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs new file mode 100644 index 000000000..c3a8d240c --- /dev/null +++ b/common/eth2/src/types.rs @@ -0,0 +1,432 @@ +//! This module exposes a superset of the `types` crate. It adds additional types that are only +//! required for the HTTP API. + +use eth2_libp2p::{Enr, Multiaddr}; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; + +pub use types::*; + +/// An API error serializable to JSON. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ErrorMessage { + pub code: u16, + pub message: String, + #[serde(default)] + pub stacktraces: Vec<String>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GenesisData { + #[serde(with = "serde_utils::quoted_u64")] + pub genesis_time: u64, + pub genesis_validators_root: Hash256, + #[serde(with = "serde_utils::bytes_4_hex")] + pub genesis_fork_version: [u8; 4], +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum BlockId { + Head, + Genesis, + Finalized, + Justified, + Slot(Slot), + Root(Hash256), +} + +impl FromStr for BlockId { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "head" => Ok(BlockId::Head), + "genesis" => Ok(BlockId::Genesis), + "finalized" => Ok(BlockId::Finalized), + "justified" => Ok(BlockId::Justified), + other => { + if other.starts_with("0x") { + Hash256::from_str(&s[2..]) + .map(BlockId::Root) + .map_err(|e| format!("{} cannot be parsed as a root", e)) + } else { + u64::from_str(s) + .map(Slot::new) + .map(BlockId::Slot) + .map_err(|_| format!("{} cannot be parsed as a parameter", s)) + } + } + } + } +} + +impl fmt::Display for BlockId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BlockId::Head => write!(f, "head"), + BlockId::Genesis => write!(f, "genesis"), + BlockId::Finalized => write!(f, "finalized"), + BlockId::Justified => write!(f, "justified"), + BlockId::Slot(slot) => write!(f, "{}", slot), + BlockId::Root(root) => write!(f, "{:?}", root), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum StateId { + Head, + Genesis, + Finalized, + Justified, + Slot(Slot), + Root(Hash256), +} + +impl FromStr for StateId { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "head" => Ok(StateId::Head), + "genesis" => Ok(StateId::Genesis), + "finalized" => Ok(StateId::Finalized), + "justified" => Ok(StateId::Justified), + other => { + if other.starts_with("0x") { + Hash256::from_str(&s[2..]) + .map(StateId::Root) + .map_err(|e| format!("{} cannot be parsed as a root", e)) + } else { + u64::from_str(s) + .map(Slot::new) + .map(StateId::Slot) + .map_err(|_| format!("{} cannot be parsed as a slot", s)) + } + } + } + } +} + +impl fmt::Display for StateId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StateId::Head => write!(f, "head"), + StateId::Genesis => write!(f, "genesis"), + StateId::Finalized => write!(f, "finalized"), + StateId::Justified => write!(f, "justified"), + StateId::Slot(slot) => write!(f, "{}", slot), + StateId::Root(root) => write!(f, "{:?}", root), + } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")] +pub struct GenericResponse<T: Serialize + serde::de::DeserializeOwned> { + pub data: T, +} + +impl<T: Serialize + serde::de::DeserializeOwned> From<T> for GenericResponse<T> { + fn from(data: T) -> Self { + Self { data } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(bound = "T: Serialize")] +pub struct GenericResponseRef<'a, T: Serialize> { + pub data: &'a T, +} + +impl<'a, T: Serialize> From<&'a T> for GenericResponseRef<'a, T> { + fn from(data: &'a T) -> Self { + Self { data } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct RootData { + pub root: Hash256, +} + +impl From<Hash256> for RootData { + fn from(root: Hash256) -> Self { + Self { root } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FinalityCheckpointsData { + pub previous_justified: Checkpoint, + pub current_justified: Checkpoint, + pub finalized: Checkpoint, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ValidatorId { + PublicKey(PublicKeyBytes), + Index(u64), +} + +impl FromStr for ValidatorId { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.starts_with("0x") { + PublicKeyBytes::from_str(s) + .map(ValidatorId::PublicKey) + .map_err(|e| format!("{} cannot be parsed as a public key: {}", s, e)) + } else { + u64::from_str(s) + .map(ValidatorId::Index) + .map_err(|e| format!("{} cannot be parsed as a slot: {}", s, e)) + } + } +} + +impl fmt::Display for ValidatorId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ValidatorId::PublicKey(pubkey) => write!(f, "{:?}", pubkey), + ValidatorId::Index(index) => write!(f, "{}", index), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorData { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub balance: u64, + pub status: ValidatorStatus, + pub validator: Validator, +} + +// TODO: This does not currently match the spec, but I'm going to try and change the spec using +// this proposal: +// +// https://hackmd.io/bQxMDRt1RbS1TLno8K4NPg?view +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ValidatorStatus { + Unknown, + WaitingForEligibility, + WaitingForFinality, + WaitingInQueue, + StandbyForActive(Epoch), + Active, + ActiveAwaitingVoluntaryExit(Epoch), + ActiveAwaitingSlashedExit(Epoch), + ExitedVoluntarily(Epoch), + ExitedSlashed(Epoch), + Withdrawable, + Withdrawn, +} + +impl ValidatorStatus { + pub fn from_validator( + validator_opt: Option<&Validator>, + epoch: Epoch, + finalized_epoch: Epoch, + far_future_epoch: Epoch, + ) -> Self { + if let Some(validator) = validator_opt { + if validator.is_withdrawable_at(epoch) { + ValidatorStatus::Withdrawable + } else if validator.is_exited_at(epoch) { + if validator.slashed { + ValidatorStatus::ExitedSlashed(validator.withdrawable_epoch) + } else { + ValidatorStatus::ExitedVoluntarily(validator.withdrawable_epoch) + } + } else if validator.is_active_at(epoch) { + if validator.exit_epoch < far_future_epoch { + if validator.slashed { + ValidatorStatus::ActiveAwaitingSlashedExit(validator.exit_epoch) + } else { + ValidatorStatus::ActiveAwaitingVoluntaryExit(validator.exit_epoch) + } + } else { + ValidatorStatus::Active + } + } else if validator.activation_epoch < far_future_epoch { + ValidatorStatus::StandbyForActive(validator.activation_epoch) + } else if validator.activation_eligibility_epoch < far_future_epoch { + if finalized_epoch < validator.activation_eligibility_epoch { + ValidatorStatus::WaitingForFinality + } else { + ValidatorStatus::WaitingInQueue + } + } else { + ValidatorStatus::WaitingForEligibility + } + } else { + ValidatorStatus::Unknown + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CommitteesQuery { + pub slot: Option<Slot>, + pub index: Option<u64>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CommitteeData { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64_vec")] + pub validators: Vec<u64>, +} + +#[derive(Serialize, Deserialize)] +pub struct HeadersQuery { + pub slot: Option<Slot>, + pub parent_root: Option<Hash256>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BlockHeaderAndSignature { + pub message: BeaconBlockHeader, + pub signature: SignatureBytes, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BlockHeaderData { + pub root: Hash256, + pub canonical: bool, + pub header: BlockHeaderAndSignature, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DepositContractData { + #[serde(with = "serde_utils::quoted_u64")] + pub chain_id: u64, + pub address: Address, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ChainHeadData { + pub slot: Slot, + pub root: Hash256, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct IdentityData { + pub peer_id: String, + pub enr: Enr, + pub p2p_addresses: Vec<Multiaddr>, + // TODO: missing the following fields: + // + // - discovery_addresses + // - metadata + // + // Tracked here: https://github.com/sigp/lighthouse/issues/1434 +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VersionData { + pub version: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SyncingData { + pub is_syncing: bool, + pub head_slot: Slot, + pub sync_distance: Slot, +} + +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(try_from = "String", bound = "T: FromStr")] +pub struct QueryVec<T: FromStr>(pub Vec<T>); + +impl<T: FromStr> TryFrom<String> for QueryVec<T> { + type Error = String; + + fn try_from(string: String) -> Result<Self, Self::Error> { + if string == "" { + return Ok(Self(vec![])); + } + + string + .split(',') + .map(|s| s.parse().map_err(|_| "unable to parse".to_string())) + .collect::<Result<Vec<T>, String>>() + .map(Self) + } +} + +#[derive(Clone, Deserialize)] +pub struct ValidatorDutiesQuery { + pub index: Option<QueryVec<u64>>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AttesterData { + pub pubkey: PublicKeyBytes, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub committees_at_slot: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub committee_index: CommitteeIndex, + #[serde(with = "serde_utils::quoted_u64")] + pub committee_length: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_committee_index: u64, + pub slot: Slot, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProposerData { + pub pubkey: PublicKeyBytes, + pub slot: Slot, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorBlocksQuery { + pub randao_reveal: SignatureBytes, + pub graffiti: Option<Graffiti>, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorAttestationDataQuery { + pub slot: Slot, + pub committee_index: CommitteeIndex, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorAggregateAttestationQuery { + pub attestation_data_root: Hash256, + pub slot: Slot, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BeaconCommitteeSubscription { + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub committee_index: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub committees_at_slot: u64, + pub slot: Slot, + pub is_aggregator: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn query_vec() { + assert_eq!( + QueryVec::try_from("0,1,2".to_string()).unwrap(), + QueryVec(vec![0_u64, 1, 2]) + ); + } +} diff --git a/common/lighthouse_metrics/src/lib.rs b/common/lighthouse_metrics/src/lib.rs index 0a4251e06..0637b973c 100644 --- a/common/lighthouse_metrics/src/lib.rs +++ b/common/lighthouse_metrics/src/lib.rs @@ -55,6 +55,7 @@ //! ``` use prometheus::{HistogramOpts, HistogramTimer, Opts}; +use std::time::Duration; pub use prometheus::{ Encoder, Gauge, GaugeVec, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, @@ -221,6 +222,19 @@ pub fn start_timer(histogram: &Result<Histogram>) -> Option<HistogramTimer> { } } +/// Starts a timer on `vec` with the given `name`. +pub fn observe_timer_vec(vec: &Result<HistogramVec>, name: &[&str], duration: Duration) { + // This conversion was taken from here: + // + // https://docs.rs/prometheus/0.5.0/src/prometheus/histogram.rs.html#550-555 + let nanos = f64::from(duration.subsec_nanos()) / 1e9; + let secs = duration.as_secs() as f64 + nanos; + + if let Some(h) = get_histogram(vec, name) { + h.observe(secs) + } +} + /// Stops a timer created with `start_timer(..)`. pub fn stop_timer(timer: Option<HistogramTimer>) { if let Some(t) = timer { diff --git a/common/remote_beacon_node/Cargo.toml b/common/remote_beacon_node/Cargo.toml deleted file mode 100644 index 38ee8c7ca..000000000 --- a/common/remote_beacon_node/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "remote_beacon_node" -version = "0.2.0" -authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -reqwest = { version = "0.10.4", features = ["json", "native-tls-vendored"] } -url = "2.1.1" -serde = "1.0.110" -futures = "0.3.5" -types = { path = "../../consensus/types" } -rest_types = { path = "../rest_types" } -hex = "0.4.2" -eth2_ssz = "0.1.2" -serde_json = "1.0.52" -eth2_config = { path = "../eth2_config" } -proto_array = { path = "../../consensus/proto_array" } -operation_pool = { path = "../../beacon_node/operation_pool" } diff --git a/common/remote_beacon_node/src/lib.rs b/common/remote_beacon_node/src/lib.rs deleted file mode 100644 index 199efefd9..000000000 --- a/common/remote_beacon_node/src/lib.rs +++ /dev/null @@ -1,732 +0,0 @@ -//! Provides a `RemoteBeaconNode` which interacts with a HTTP API on another Lighthouse (or -//! compatible) instance. -//! -//! Presently, this is only used for testing but it _could_ become a user-facing library. - -use eth2_config::Eth2Config; -use reqwest::{Client, ClientBuilder, Response, StatusCode}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use ssz::Encode; -use std::marker::PhantomData; -use std::time::Duration; -use types::{ - Attestation, AttestationData, AttesterSlashing, BeaconBlock, BeaconState, CommitteeIndex, - Epoch, EthSpec, Fork, Graffiti, Hash256, ProposerSlashing, PublicKey, PublicKeyBytes, - Signature, SignedAggregateAndProof, SignedBeaconBlock, Slot, SubnetId, -}; -use url::Url; - -pub use operation_pool::PersistedOperationPool; -pub use proto_array::core::ProtoArray; -pub use rest_types::{ - CanonicalHeadResponse, Committee, HeadBeaconBlock, Health, IndividualVotesRequest, - IndividualVotesResponse, SyncingResponse, ValidatorDutiesRequest, ValidatorDutyBytes, - ValidatorRequest, ValidatorResponse, ValidatorSubscription, -}; - -// Setting a long timeout for debug ensures that crypto-heavy operations can still succeed. -#[cfg(debug_assertions)] -pub const REQUEST_TIMEOUT_SECONDS: u64 = 15; - -#[cfg(not(debug_assertions))] -pub const REQUEST_TIMEOUT_SECONDS: u64 = 5; - -#[derive(Clone)] -/// Connects to a remote Lighthouse (or compatible) node via HTTP. -pub struct RemoteBeaconNode<E: EthSpec> { - pub http: HttpClient<E>, -} - -impl<E: EthSpec> RemoteBeaconNode<E> { - /// Uses the default HTTP timeout. - pub fn new(http_endpoint: String) -> Result<Self, String> { - Self::new_with_timeout(http_endpoint, Duration::from_secs(REQUEST_TIMEOUT_SECONDS)) - } - - pub fn new_with_timeout(http_endpoint: String, timeout: Duration) -> Result<Self, String> { - Ok(Self { - http: HttpClient::new(http_endpoint, timeout) - .map_err(|e| format!("Unable to create http client: {:?}", e))?, - }) - } -} - -#[derive(Debug)] -pub enum Error { - /// Unable to parse a URL. Check the server URL. - UrlParseError(url::ParseError), - /// The `reqwest` library returned an error. - ReqwestError(reqwest::Error), - /// There was an error when encoding/decoding an object using serde. - SerdeJsonError(serde_json::Error), - /// The server responded to the request, however it did not return a 200-type success code. - DidNotSucceed { status: StatusCode, body: String }, - /// The request input was invalid. - InvalidInput, -} - -#[derive(Clone)] -pub struct HttpClient<E> { - client: Client, - url: Url, - timeout: Duration, - _phantom: PhantomData<E>, -} - -impl<E: EthSpec> HttpClient<E> { - /// Creates a new instance (without connecting to the node). - pub fn new(server_url: String, timeout: Duration) -> Result<Self, Error> { - Ok(Self { - client: ClientBuilder::new() - .timeout(timeout) - .build() - .expect("should build from static configuration"), - url: Url::parse(&server_url)?, - timeout: Duration::from_secs(15), - _phantom: PhantomData, - }) - } - - pub fn beacon(&self) -> Beacon<E> { - Beacon(self.clone()) - } - - pub fn validator(&self) -> Validator<E> { - Validator(self.clone()) - } - - pub fn spec(&self) -> Spec<E> { - Spec(self.clone()) - } - - pub fn node(&self) -> Node<E> { - Node(self.clone()) - } - - pub fn advanced(&self) -> Advanced<E> { - Advanced(self.clone()) - } - - pub fn consensus(&self) -> Consensus<E> { - Consensus(self.clone()) - } - - fn url(&self, path: &str) -> Result<Url, Error> { - self.url.join(path).map_err(|e| e.into()) - } - - pub async fn json_post<T: Serialize>(&self, url: Url, body: T) -> Result<Response, Error> { - self.client - .post(&url.to_string()) - .json(&body) - .send() - .await - .map_err(Error::from) - } - - pub async fn json_get<T: DeserializeOwned>( - &self, - mut url: Url, - query_pairs: Vec<(String, String)>, - ) -> Result<T, Error> { - query_pairs.into_iter().for_each(|(key, param)| { - url.query_pairs_mut().append_pair(&key, ¶m); - }); - - let response = self - .client - .get(&url.to_string()) - .send() - .await - .map_err(Error::from)?; - - let success = error_for_status(response).await.map_err(Error::from)?; - success.json::<T>().await.map_err(Error::from) - } -} - -/// Returns an `Error` (with a description) if the `response` was not a 200-type success response. -/// -/// Distinct from `Response::error_for_status` because it includes the body of the response as -/// text. This ensures the error message from the server is not discarded. -async fn error_for_status(response: Response) -> Result<Response, Error> { - let status = response.status(); - - if status.is_success() { - Ok(response) - } else { - let text_result = response.text().await; - match text_result { - Err(e) => Err(Error::ReqwestError(e)), - Ok(body) => Err(Error::DidNotSucceed { status, body }), - } - } -} - -#[derive(Debug, PartialEq, Clone)] -pub enum PublishStatus { - /// The object was valid and has been published to the network. - Valid, - /// The object was not valid and may or may not have been published to the network. - Invalid(String), - /// The server responded with an unknown status code. The object may or may not have been - /// published to the network. - Unknown, -} - -impl PublishStatus { - /// Returns `true` if `*self == PublishStatus::Valid`. - pub fn is_valid(&self) -> bool { - *self == PublishStatus::Valid - } -} - -/// Provides the functions on the `/validator` endpoint of the node. -#[derive(Clone)] -pub struct Validator<E>(HttpClient<E>); - -impl<E: EthSpec> Validator<E> { - fn url(&self, path: &str) -> Result<Url, Error> { - self.0 - .url("validator/") - .and_then(move |url| url.join(path).map_err(Error::from)) - .map_err(Into::into) - } - - /// Produces an unsigned attestation. - pub async fn produce_attestation( - &self, - slot: Slot, - committee_index: CommitteeIndex, - ) -> Result<Attestation<E>, Error> { - let query_params = vec![ - ("slot".into(), format!("{}", slot)), - ("committee_index".into(), format!("{}", committee_index)), - ]; - - let client = self.0.clone(); - let url = self.url("attestation")?; - client.json_get(url, query_params).await - } - - /// Produces an aggregate attestation. - pub async fn produce_aggregate_attestation( - &self, - attestation_data: &AttestationData, - ) -> Result<Attestation<E>, Error> { - let query_params = vec![( - "attestation_data".into(), - as_ssz_hex_string(attestation_data), - )]; - - let client = self.0.clone(); - let url = self.url("aggregate_attestation")?; - client.json_get(url, query_params).await - } - - /// Posts a list of attestations to the beacon node, expecting it to verify it and publish it to the network. - pub async fn publish_attestations( - &self, - attestation: Vec<(Attestation<E>, SubnetId)>, - ) -> Result<PublishStatus, Error> { - let client = self.0.clone(); - let url = self.url("attestations")?; - let response = client.json_post::<_>(url, attestation).await?; - - match response.status() { - StatusCode::OK => Ok(PublishStatus::Valid), - StatusCode::ACCEPTED => Ok(PublishStatus::Invalid( - response.text().await.map_err(Error::from)?, - )), - _ => response - .error_for_status() - .map_err(Error::from) - .map(|_| PublishStatus::Unknown), - } - } - - /// Posts a list of signed aggregates and proofs to the beacon node, expecting it to verify it and publish it to the network. - pub async fn publish_aggregate_and_proof( - &self, - signed_aggregate_and_proofs: Vec<SignedAggregateAndProof<E>>, - ) -> Result<PublishStatus, Error> { - let client = self.0.clone(); - let url = self.url("aggregate_and_proofs")?; - let response = client - .json_post::<_>(url, signed_aggregate_and_proofs) - .await?; - - match response.status() { - StatusCode::OK => Ok(PublishStatus::Valid), - StatusCode::ACCEPTED => Ok(PublishStatus::Invalid( - response.text().await.map_err(Error::from)?, - )), - _ => response - .error_for_status() - .map_err(Error::from) - .map(|_| PublishStatus::Unknown), - } - } - - /// Returns the duties required of the given validator pubkeys in the given epoch. - pub async fn get_duties( - &self, - epoch: Epoch, - validator_pubkeys: &[PublicKey], - ) -> Result<Vec<ValidatorDutyBytes>, Error> { - let client = self.0.clone(); - - let bulk_request = ValidatorDutiesRequest { - epoch, - pubkeys: validator_pubkeys - .iter() - .map(|pubkey| pubkey.clone().into()) - .collect(), - }; - - let url = self.url("duties")?; - let response = client.json_post::<_>(url, bulk_request).await?; - let success = error_for_status(response).await.map_err(Error::from)?; - success.json().await.map_err(Error::from) - } - - /// Posts a block to the beacon node, expecting it to verify it and publish it to the network. - pub async fn publish_block(&self, block: SignedBeaconBlock<E>) -> Result<PublishStatus, Error> { - let client = self.0.clone(); - let url = self.url("block")?; - let response = client.json_post::<_>(url, block).await?; - - match response.status() { - StatusCode::OK => Ok(PublishStatus::Valid), - StatusCode::ACCEPTED => Ok(PublishStatus::Invalid( - response.text().await.map_err(Error::from)?, - )), - _ => response - .error_for_status() - .map_err(Error::from) - .map(|_| PublishStatus::Unknown), - } - } - - /// Requests a new (unsigned) block from the beacon node. - pub async fn produce_block( - &self, - slot: Slot, - randao_reveal: Signature, - graffiti: Option<Graffiti>, - ) -> Result<BeaconBlock<E>, Error> { - let client = self.0.clone(); - let url = self.url("block")?; - - let mut query_pairs = vec![ - ("slot".into(), format!("{}", slot.as_u64())), - ("randao_reveal".into(), as_ssz_hex_string(&randao_reveal)), - ]; - - if let Some(graffiti_bytes) = graffiti { - query_pairs.push(("graffiti".into(), as_ssz_hex_string(&graffiti_bytes))); - } - - client.json_get::<BeaconBlock<E>>(url, query_pairs).await - } - - /// Subscribes a list of validators to particular slots for attestation production/publication. - pub async fn subscribe( - &self, - subscriptions: Vec<ValidatorSubscription>, - ) -> Result<PublishStatus, Error> { - let client = self.0.clone(); - let url = self.url("subscribe")?; - let response = client.json_post::<_>(url, subscriptions).await?; - - match response.status() { - StatusCode::OK => Ok(PublishStatus::Valid), - StatusCode::ACCEPTED => Ok(PublishStatus::Invalid( - response.text().await.map_err(Error::from)?, - )), - _ => response - .error_for_status() - .map_err(Error::from) - .map(|_| PublishStatus::Unknown), - } - } -} - -/// Provides the functions on the `/beacon` endpoint of the node. -#[derive(Clone)] -pub struct Beacon<E>(HttpClient<E>); - -impl<E: EthSpec> Beacon<E> { - fn url(&self, path: &str) -> Result<Url, Error> { - self.0 - .url("beacon/") - .and_then(move |url| url.join(path).map_err(Error::from)) - .map_err(Into::into) - } - - /// Returns the genesis time. - pub async fn get_genesis_time(&self) -> Result<u64, Error> { - let client = self.0.clone(); - let url = self.url("genesis_time")?; - client.json_get(url, vec![]).await - } - - /// Returns the genesis validators root. - pub async fn get_genesis_validators_root(&self) -> Result<Hash256, Error> { - let client = self.0.clone(); - let url = self.url("genesis_validators_root")?; - client.json_get(url, vec![]).await - } - - /// Returns the fork at the head of the beacon chain. - pub async fn get_fork(&self) -> Result<Fork, Error> { - let client = self.0.clone(); - let url = self.url("fork")?; - client.json_get(url, vec![]).await - } - - /// Returns info about the head of the canonical beacon chain. - pub async fn get_head(&self) -> Result<CanonicalHeadResponse, Error> { - let client = self.0.clone(); - let url = self.url("head")?; - client.json_get::<CanonicalHeadResponse>(url, vec![]).await - } - - /// Returns the set of known beacon chain head blocks. One of these will be the canonical head. - pub async fn get_heads(&self) -> Result<Vec<HeadBeaconBlock>, Error> { - let client = self.0.clone(); - let url = self.url("heads")?; - client.json_get(url, vec![]).await - } - - /// Returns the block and block root at the given slot. - pub async fn get_block_by_slot( - &self, - slot: Slot, - ) -> Result<(SignedBeaconBlock<E>, Hash256), Error> { - self.get_block("slot".to_string(), format!("{}", slot.as_u64())) - .await - } - - /// Returns the block and block root at the given root. - pub async fn get_block_by_root( - &self, - root: Hash256, - ) -> Result<(SignedBeaconBlock<E>, Hash256), Error> { - self.get_block("root".to_string(), root_as_string(root)) - .await - } - - /// Returns the block and block root at the given slot. - async fn get_block( - &self, - query_key: String, - query_param: String, - ) -> Result<(SignedBeaconBlock<E>, Hash256), Error> { - let client = self.0.clone(); - let url = self.url("block")?; - client - .json_get::<BlockResponse<E>>(url, vec![(query_key, query_param)]) - .await - .map(|response| (response.beacon_block, response.root)) - } - - /// Returns the state and state root at the given slot. - pub async fn get_state_by_slot(&self, slot: Slot) -> Result<(BeaconState<E>, Hash256), Error> { - self.get_state("slot".to_string(), format!("{}", slot.as_u64())) - .await - } - - /// Returns the state and state root at the given root. - pub async fn get_state_by_root( - &self, - root: Hash256, - ) -> Result<(BeaconState<E>, Hash256), Error> { - self.get_state("root".to_string(), root_as_string(root)) - .await - } - - /// Returns the root of the state at the given slot. - pub async fn get_state_root(&self, slot: Slot) -> Result<Hash256, Error> { - let client = self.0.clone(); - let url = self.url("state_root")?; - client - .json_get(url, vec![("slot".into(), format!("{}", slot.as_u64()))]) - .await - } - - /// Returns the root of the block at the given slot. - pub async fn get_block_root(&self, slot: Slot) -> Result<Hash256, Error> { - let client = self.0.clone(); - let url = self.url("block_root")?; - client - .json_get(url, vec![("slot".into(), format!("{}", slot.as_u64()))]) - .await - } - - /// Returns the state and state root at the given slot. - async fn get_state( - &self, - query_key: String, - query_param: String, - ) -> Result<(BeaconState<E>, Hash256), Error> { - let client = self.0.clone(); - let url = self.url("state")?; - client - .json_get::<StateResponse<E>>(url, vec![(query_key, query_param)]) - .await - .map(|response| (response.beacon_state, response.root)) - } - - /// Returns the block and block root at the given slot. - /// - /// If `state_root` is `Some`, the query will use the given state instead of the default - /// canonical head state. - pub async fn get_validators( - &self, - validator_pubkeys: Vec<PublicKey>, - state_root: Option<Hash256>, - ) -> Result<Vec<ValidatorResponse>, Error> { - let client = self.0.clone(); - - let bulk_request = ValidatorRequest { - state_root, - pubkeys: validator_pubkeys - .iter() - .map(|pubkey| pubkey.clone().into()) - .collect(), - }; - - let url = self.url("validators")?; - let response = client.json_post::<_>(url, bulk_request).await?; - let success = error_for_status(response).await.map_err(Error::from)?; - success.json().await.map_err(Error::from) - } - - /// Returns all validators. - /// - /// If `state_root` is `Some`, the query will use the given state instead of the default - /// canonical head state. - pub async fn get_all_validators( - &self, - state_root: Option<Hash256>, - ) -> Result<Vec<ValidatorResponse>, Error> { - let client = self.0.clone(); - - let query_params = if let Some(state_root) = state_root { - vec![("state_root".into(), root_as_string(state_root))] - } else { - vec![] - }; - - let url = self.url("validators/all")?; - client.json_get(url, query_params).await - } - - /// Returns the active validators. - /// - /// If `state_root` is `Some`, the query will use the given state instead of the default - /// canonical head state. - pub async fn get_active_validators( - &self, - state_root: Option<Hash256>, - ) -> Result<Vec<ValidatorResponse>, Error> { - let client = self.0.clone(); - - let query_params = if let Some(state_root) = state_root { - vec![("state_root".into(), root_as_string(state_root))] - } else { - vec![] - }; - - let url = self.url("validators/active")?; - client.json_get(url, query_params).await - } - - /// Returns committees at the given epoch. - pub async fn get_committees(&self, epoch: Epoch) -> Result<Vec<Committee>, Error> { - let client = self.0.clone(); - - let url = self.url("committees")?; - client - .json_get(url, vec![("epoch".into(), format!("{}", epoch.as_u64()))]) - .await - } - - pub async fn proposer_slashing( - &self, - proposer_slashing: ProposerSlashing, - ) -> Result<bool, Error> { - let client = self.0.clone(); - - let url = self.url("proposer_slashing")?; - let response = client.json_post::<_>(url, proposer_slashing).await?; - let success = error_for_status(response).await.map_err(Error::from)?; - success.json().await.map_err(Error::from) - } - - pub async fn attester_slashing( - &self, - attester_slashing: AttesterSlashing<E>, - ) -> Result<bool, Error> { - let client = self.0.clone(); - - let url = self.url("attester_slashing")?; - let response = client.json_post::<_>(url, attester_slashing).await?; - let success = error_for_status(response).await.map_err(Error::from)?; - success.json().await.map_err(Error::from) - } -} - -/// Provides the functions on the `/spec` endpoint of the node. -#[derive(Clone)] -pub struct Spec<E>(HttpClient<E>); - -impl<E: EthSpec> Spec<E> { - fn url(&self, path: &str) -> Result<Url, Error> { - self.0 - .url("spec/") - .and_then(move |url| url.join(path).map_err(Error::from)) - .map_err(Into::into) - } - - pub async fn get_eth2_config(&self) -> Result<Eth2Config, Error> { - let client = self.0.clone(); - let url = self.url("eth2_config")?; - client.json_get(url, vec![]).await - } -} - -/// Provides the functions on the `/node` endpoint of the node. -#[derive(Clone)] -pub struct Node<E>(HttpClient<E>); - -impl<E: EthSpec> Node<E> { - fn url(&self, path: &str) -> Result<Url, Error> { - self.0 - .url("node/") - .and_then(move |url| url.join(path).map_err(Error::from)) - .map_err(Into::into) - } - - pub async fn get_version(&self) -> Result<String, Error> { - let client = self.0.clone(); - let url = self.url("version")?; - client.json_get(url, vec![]).await - } - - pub async fn get_health(&self) -> Result<Health, Error> { - let client = self.0.clone(); - let url = self.url("health")?; - client.json_get(url, vec![]).await - } - - pub async fn syncing_status(&self) -> Result<SyncingResponse, Error> { - let client = self.0.clone(); - let url = self.url("syncing")?; - client.json_get(url, vec![]).await - } -} - -/// Provides the functions on the `/advanced` endpoint of the node. -#[derive(Clone)] -pub struct Advanced<E>(HttpClient<E>); - -impl<E: EthSpec> Advanced<E> { - fn url(&self, path: &str) -> Result<Url, Error> { - self.0 - .url("advanced/") - .and_then(move |url| url.join(path).map_err(Error::from)) - .map_err(Into::into) - } - - /// Gets the core `ProtoArray` struct from the node. - pub async fn get_fork_choice(&self) -> Result<ProtoArray, Error> { - let client = self.0.clone(); - let url = self.url("fork_choice")?; - client.json_get(url, vec![]).await - } - - /// Gets the core `PersistedOperationPool` struct from the node. - pub async fn get_operation_pool(&self) -> Result<PersistedOperationPool<E>, Error> { - let client = self.0.clone(); - let url = self.url("operation_pool")?; - client.json_get(url, vec![]).await - } -} - -/// Provides the functions on the `/consensus` endpoint of the node. -#[derive(Clone)] -pub struct Consensus<E>(HttpClient<E>); - -impl<E: EthSpec> Consensus<E> { - fn url(&self, path: &str) -> Result<Url, Error> { - self.0 - .url("consensus/") - .and_then(move |url| url.join(path).map_err(Error::from)) - .map_err(Into::into) - } - - /// Gets a `IndividualVote` for each of the given `pubkeys`. - pub async fn get_individual_votes( - &self, - epoch: Epoch, - pubkeys: Vec<PublicKeyBytes>, - ) -> Result<IndividualVotesResponse, Error> { - let client = self.0.clone(); - let req_body = IndividualVotesRequest { epoch, pubkeys }; - - let url = self.url("individual_votes")?; - let response = client.json_post::<_>(url, req_body).await?; - let success = error_for_status(response).await.map_err(Error::from)?; - success.json().await.map_err(Error::from) - } - - /// Gets a `VoteCount` for the given `epoch`. - pub async fn get_vote_count(&self, epoch: Epoch) -> Result<IndividualVotesResponse, Error> { - let client = self.0.clone(); - let query_params = vec![("epoch".into(), format!("{}", epoch.as_u64()))]; - let url = self.url("vote_count")?; - client.json_get(url, query_params).await - } -} - -#[derive(Deserialize)] -#[serde(bound = "T: EthSpec")] -pub struct BlockResponse<T: EthSpec> { - pub beacon_block: SignedBeaconBlock<T>, - pub root: Hash256, -} - -#[derive(Deserialize)] -#[serde(bound = "T: EthSpec")] -pub struct StateResponse<T: EthSpec> { - pub beacon_state: BeaconState<T>, - pub root: Hash256, -} - -fn root_as_string(root: Hash256) -> String { - format!("0x{:?}", root) -} - -fn as_ssz_hex_string<T: Encode>(item: &T) -> String { - format!("0x{}", hex::encode(item.as_ssz_bytes())) -} - -impl From<reqwest::Error> for Error { - fn from(e: reqwest::Error) -> Error { - Error::ReqwestError(e) - } -} - -impl From<url::ParseError> for Error { - fn from(e: url::ParseError) -> Error { - Error::UrlParseError(e) - } -} - -impl From<serde_json::Error> for Error { - fn from(e: serde_json::Error) -> Error { - Error::SerdeJsonError(e) - } -} diff --git a/common/rest_types/Cargo.toml b/common/rest_types/Cargo.toml deleted file mode 100644 index d9e021fe1..000000000 --- a/common/rest_types/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "rest_types" -version = "0.2.0" -authors = ["Sigma Prime <contact@sigmaprime.io>"] -edition = "2018" - -[dependencies] -types = { path = "../../consensus/types" } -eth2_ssz_derive = "0.1.0" -eth2_ssz = "0.1.2" -eth2_hashing = "0.1.0" -tree_hash = "0.1.0" -state_processing = { path = "../../consensus/state_processing" } -bls = { path = "../../crypto/bls" } -serde = { version = "1.0.110", features = ["derive"] } -rayon = "1.3.0" -hyper = "0.13.5" -tokio = { version = "0.2.21", features = ["sync"] } -environment = { path = "../../lighthouse/environment" } -store = { path = "../../beacon_node/store" } -beacon_chain = { path = "../../beacon_node/beacon_chain" } -serde_json = "1.0.52" -serde_yaml = "0.8.11" - -[target.'cfg(target_os = "linux")'.dependencies] -psutil = "3.1.0" -procinfo = "0.4.2" diff --git a/common/rest_types/src/api_error.rs b/common/rest_types/src/api_error.rs deleted file mode 100644 index 1eac8d4a4..000000000 --- a/common/rest_types/src/api_error.rs +++ /dev/null @@ -1,99 +0,0 @@ -use hyper::{Body, Response, StatusCode}; -use std::error::Error as StdError; - -#[derive(PartialEq, Debug, Clone)] -pub enum ApiError { - MethodNotAllowed(String), - ServerError(String), - NotImplemented(String), - BadRequest(String), - NotFound(String), - UnsupportedType(String), - ImATeapot(String), // Just in case. - ProcessingError(String), // A 202 error, for when a block/attestation cannot be processed, but still transmitted. - InvalidHeaderValue(String), -} - -pub type ApiResult = Result<Response<Body>, ApiError>; - -impl ApiError { - pub fn status_code(self) -> (StatusCode, String) { - match self { - ApiError::MethodNotAllowed(desc) => (StatusCode::METHOD_NOT_ALLOWED, desc), - ApiError::ServerError(desc) => (StatusCode::INTERNAL_SERVER_ERROR, desc), - ApiError::NotImplemented(desc) => (StatusCode::NOT_IMPLEMENTED, desc), - ApiError::BadRequest(desc) => (StatusCode::BAD_REQUEST, desc), - ApiError::NotFound(desc) => (StatusCode::NOT_FOUND, desc), - ApiError::UnsupportedType(desc) => (StatusCode::UNSUPPORTED_MEDIA_TYPE, desc), - ApiError::ImATeapot(desc) => (StatusCode::IM_A_TEAPOT, desc), - ApiError::ProcessingError(desc) => (StatusCode::ACCEPTED, desc), - ApiError::InvalidHeaderValue(desc) => (StatusCode::INTERNAL_SERVER_ERROR, desc), - } - } -} - -impl Into<Response<Body>> for ApiError { - fn into(self) -> Response<Body> { - let (status_code, desc) = self.status_code(); - Response::builder() - .status(status_code) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from(desc)) - .expect("Response should always be created.") - } -} - -impl From<store::Error> for ApiError { - fn from(e: store::Error) -> ApiError { - ApiError::ServerError(format!("Database error: {:?}", e)) - } -} - -impl From<types::BeaconStateError> for ApiError { - fn from(e: types::BeaconStateError) -> ApiError { - ApiError::ServerError(format!("BeaconState error: {:?}", e)) - } -} - -impl From<beacon_chain::BeaconChainError> for ApiError { - fn from(e: beacon_chain::BeaconChainError) -> ApiError { - ApiError::ServerError(format!("BeaconChainError error: {:?}", e)) - } -} - -impl From<state_processing::per_slot_processing::Error> for ApiError { - fn from(e: state_processing::per_slot_processing::Error) -> ApiError { - ApiError::ServerError(format!("PerSlotProcessing error: {:?}", e)) - } -} - -impl From<hyper::error::Error> for ApiError { - fn from(e: hyper::error::Error) -> ApiError { - ApiError::ServerError(format!("Networking error: {:?}", e)) - } -} - -impl From<std::io::Error> for ApiError { - fn from(e: std::io::Error) -> ApiError { - ApiError::ServerError(format!("IO error: {:?}", e)) - } -} - -impl From<hyper::header::InvalidHeaderValue> for ApiError { - fn from(e: hyper::header::InvalidHeaderValue) -> ApiError { - ApiError::InvalidHeaderValue(format!("Invalid CORS header value: {:?}", e)) - } -} - -impl StdError for ApiError { - fn cause(&self) -> Option<&dyn StdError> { - None - } -} - -impl std::fmt::Display for ApiError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let status = self.clone().status_code(); - write!(f, "{:?}: {:?}", status.0, status.1) - } -} diff --git a/common/rest_types/src/beacon.rs b/common/rest_types/src/beacon.rs deleted file mode 100644 index 0a141ea28..000000000 --- a/common/rest_types/src/beacon.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! A collection of REST API types for interaction with the beacon node. - -use bls::PublicKeyBytes; -use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use types::beacon_state::EthSpec; -use types::{BeaconState, CommitteeIndex, Hash256, SignedBeaconBlock, Slot, Validator}; - -/// Information about a block that is at the head of a chain. May or may not represent the -/// canonical head. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -pub struct HeadBeaconBlock { - pub beacon_block_root: Hash256, - pub beacon_block_slot: Slot, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -#[serde(bound = "T: EthSpec")] -pub struct BlockResponse<T: EthSpec> { - pub root: Hash256, - pub beacon_block: SignedBeaconBlock<T>, -} - -/// Information about the block and state that are at head of the beacon chain. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -pub struct CanonicalHeadResponse { - pub slot: Slot, - pub block_root: Hash256, - pub state_root: Hash256, - pub finalized_slot: Slot, - pub finalized_block_root: Hash256, - pub justified_slot: Slot, - pub justified_block_root: Hash256, - pub previous_justified_slot: Slot, - pub previous_justified_block_root: Hash256, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -pub struct ValidatorResponse { - pub pubkey: PublicKeyBytes, - pub validator_index: Option<usize>, - pub balance: Option<u64>, - pub validator: Option<Validator>, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -pub struct ValidatorRequest { - /// If set to `None`, uses the canonical head state. - pub state_root: Option<Hash256>, - pub pubkeys: Vec<PublicKeyBytes>, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -pub struct Committee { - pub slot: Slot, - pub index: CommitteeIndex, - pub committee: Vec<usize>, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -#[serde(bound = "T: EthSpec")] -pub struct StateResponse<T: EthSpec> { - pub root: Hash256, - pub beacon_state: BeaconState<T>, -} diff --git a/common/rest_types/src/consensus.rs b/common/rest_types/src/consensus.rs deleted file mode 100644 index 519b1ae24..000000000 --- a/common/rest_types/src/consensus.rs +++ /dev/null @@ -1,66 +0,0 @@ -use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_epoch_processing::ValidatorStatus; -use types::{Epoch, PublicKeyBytes}; - -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] -pub struct IndividualVotesRequest { - pub epoch: Epoch, - pub pubkeys: Vec<PublicKeyBytes>, -} - -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] -pub struct IndividualVote { - /// True if the validator has been slashed, ever. - pub is_slashed: bool, - /// True if the validator can withdraw in the current epoch. - pub is_withdrawable_in_current_epoch: bool, - /// True if the validator was active in the state's _current_ epoch. - pub is_active_in_current_epoch: bool, - /// True if the validator was active in the state's _previous_ epoch. - pub is_active_in_previous_epoch: bool, - /// The validator's effective balance in the _current_ epoch. - pub current_epoch_effective_balance_gwei: u64, - /// True if the validator had an attestation included in the _current_ epoch. - pub is_current_epoch_attester: bool, - /// True if the validator's beacon block root attestation for the first slot of the _current_ - /// epoch matches the block root known to the state. - pub is_current_epoch_target_attester: bool, - /// True if the validator had an attestation included in the _previous_ epoch. - pub is_previous_epoch_attester: bool, - /// True if the validator's beacon block root attestation for the first slot of the _previous_ - /// epoch matches the block root known to the state. - pub is_previous_epoch_target_attester: bool, - /// True if the validator's beacon block root attestation in the _previous_ epoch at the - /// attestation's slot (`attestation_data.slot`) matches the block root known to the state. - pub is_previous_epoch_head_attester: bool, -} - -impl Into<IndividualVote> for ValidatorStatus { - fn into(self) -> IndividualVote { - IndividualVote { - is_slashed: self.is_slashed, - is_withdrawable_in_current_epoch: self.is_withdrawable_in_current_epoch, - is_active_in_current_epoch: self.is_active_in_current_epoch, - is_active_in_previous_epoch: self.is_active_in_previous_epoch, - current_epoch_effective_balance_gwei: self.current_epoch_effective_balance, - is_current_epoch_attester: self.is_current_epoch_attester, - is_current_epoch_target_attester: self.is_current_epoch_target_attester, - is_previous_epoch_attester: self.is_previous_epoch_attester, - is_previous_epoch_target_attester: self.is_previous_epoch_target_attester, - is_previous_epoch_head_attester: self.is_previous_epoch_head_attester, - } - } -} - -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] -pub struct IndividualVotesResponse { - /// The epoch which is considered the "current" epoch. - pub epoch: Epoch, - /// The validators public key. - pub pubkey: PublicKeyBytes, - /// The index of the validator in state.validators. - pub validator_index: Option<usize>, - /// Voting statistics for the validator, if they voted in the given epoch. - pub vote: Option<IndividualVote>, -} diff --git a/common/rest_types/src/handler.rs b/common/rest_types/src/handler.rs deleted file mode 100644 index cbbcd73b1..000000000 --- a/common/rest_types/src/handler.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::{ApiError, ApiResult}; -use environment::TaskExecutor; -use hyper::header; -use hyper::{Body, Request, Response, StatusCode}; -use serde::Deserialize; -use serde::Serialize; -use ssz::Encode; - -/// Defines the encoding for the API. -#[derive(Clone, Serialize, Deserialize, Copy)] -pub enum ApiEncodingFormat { - JSON, - YAML, - SSZ, -} - -impl ApiEncodingFormat { - pub fn get_content_type(&self) -> &str { - match self { - ApiEncodingFormat::JSON => "application/json", - ApiEncodingFormat::YAML => "application/yaml", - ApiEncodingFormat::SSZ => "application/ssz", - } - } -} - -impl From<&str> for ApiEncodingFormat { - fn from(f: &str) -> ApiEncodingFormat { - match f { - "application/yaml" => ApiEncodingFormat::YAML, - "application/ssz" => ApiEncodingFormat::SSZ, - _ => ApiEncodingFormat::JSON, - } - } -} - -/// Provides a HTTP request handler with Lighthouse-specific functionality. -pub struct Handler<T> { - executor: TaskExecutor, - req: Request<()>, - body: Body, - ctx: T, - encoding: ApiEncodingFormat, - allow_body: bool, -} - -impl<T: Clone + Send + Sync + 'static> Handler<T> { - /// Start handling a new request. - pub fn new(req: Request<Body>, ctx: T, executor: TaskExecutor) -> Result<Self, ApiError> { - let (req_parts, body) = req.into_parts(); - let req = Request::from_parts(req_parts, ()); - - let accept_header: String = req - .headers() - .get(header::ACCEPT) - .map_or(Ok(""), |h| h.to_str()) - .map_err(|e| { - ApiError::BadRequest(format!( - "The Accept header contains invalid characters: {:?}", - e - )) - }) - .map(String::from)?; - - Ok(Self { - executor, - req, - body, - ctx, - allow_body: false, - encoding: ApiEncodingFormat::from(accept_header.as_str()), - }) - } - - /// The default behaviour is to return an error if any body is supplied in the request. Calling - /// this function disables that error. - pub fn allow_body(mut self) -> Self { - self.allow_body = true; - self - } - - /// Return a simple static value. - /// - /// Does not use the blocking executor. - pub async fn static_value<V>(self, value: V) -> Result<HandledRequest<V>, ApiError> { - // Always check and disallow a body for a static value. - let _ = Self::get_body(self.body, false).await?; - - Ok(HandledRequest { - value, - encoding: self.encoding, - }) - } - - /// Calls `func` in-line, on the core executor. - /// - /// This should only be used for very fast tasks. - pub async fn in_core_task<F, V>(self, func: F) -> Result<HandledRequest<V>, ApiError> - where - V: Send + Sync + 'static, - F: Fn(Request<Vec<u8>>, T) -> Result<V, ApiError> + Send + Sync + 'static, - { - let body = Self::get_body(self.body, self.allow_body).await?; - let (req_parts, _) = self.req.into_parts(); - let req = Request::from_parts(req_parts, body); - - let value = func(req, self.ctx)?; - - Ok(HandledRequest { - value, - encoding: self.encoding, - }) - } - - /// Spawns `func` on the blocking executor. - /// - /// This method is suitable for handling long-running or intensive tasks. - pub async fn in_blocking_task<F, V>(self, func: F) -> Result<HandledRequest<V>, ApiError> - where - V: Send + Sync + 'static, - F: Fn(Request<Vec<u8>>, T) -> Result<V, ApiError> + Send + Sync + 'static, - { - let ctx = self.ctx; - let body = Self::get_body(self.body, self.allow_body).await?; - let (req_parts, _) = self.req.into_parts(); - let req = Request::from_parts(req_parts, body); - - let value = self - .executor - .clone() - .handle - .spawn_blocking(move || func(req, ctx)) - .await - .map_err(|e| { - ApiError::ServerError(format!( - "Failed to get blocking join handle: {}", - e.to_string() - )) - })??; - - Ok(HandledRequest { - value, - encoding: self.encoding, - }) - } - - /// Call `func`, then return a response that is suitable for an SSE stream. - pub async fn sse_stream<F>(self, func: F) -> ApiResult - where - F: Fn(Request<()>, T) -> Result<Body, ApiError>, - { - let body = func(self.req, self.ctx)?; - - Response::builder() - .status(200) - .header("Content-Type", "text/event-stream") - .header("Connection", "Keep-Alive") - .header("Cache-Control", "no-cache") - .header("Access-Control-Allow-Origin", "*") - .body(body) - .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))) - } - - /// Downloads the bytes for `body`. - async fn get_body(body: Body, allow_body: bool) -> Result<Vec<u8>, ApiError> { - let bytes = hyper::body::to_bytes(body) - .await - .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}", e)))?; - - if !allow_body && !bytes[..].is_empty() { - Err(ApiError::BadRequest( - "The request body must be empty".to_string(), - )) - } else { - Ok(bytes.into_iter().collect()) - } - } -} - -/// A request that has been "handled" and now a result (`value`) needs to be serialize and -/// returned. -pub struct HandledRequest<V> { - encoding: ApiEncodingFormat, - value: V, -} - -impl HandledRequest<String> { - /// Simple encode a string as utf-8. - pub fn text_encoding(self) -> ApiResult { - Response::builder() - .status(StatusCode::OK) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from(self.value)) - .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))) - } -} - -impl<V: Serialize + Encode> HandledRequest<V> { - /// Suitable for all items which implement `serde` and `ssz`. - pub fn all_encodings(self) -> ApiResult { - match self.encoding { - ApiEncodingFormat::SSZ => Response::builder() - .status(StatusCode::OK) - .header("content-type", "application/ssz") - .body(Body::from(self.value.as_ssz_bytes())) - .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))), - _ => self.serde_encodings(), - } - } -} - -impl<V: Serialize> HandledRequest<V> { - /// Suitable for items which only implement `serde`. - pub fn serde_encodings(self) -> ApiResult { - let (body, content_type) = match self.encoding { - ApiEncodingFormat::JSON => ( - Body::from(serde_json::to_string(&self.value).map_err(|e| { - ApiError::ServerError(format!( - "Unable to serialize response body as JSON: {:?}", - e - )) - })?), - "application/json", - ), - ApiEncodingFormat::SSZ => { - return Err(ApiError::UnsupportedType( - "Response cannot be encoded as SSZ.".into(), - )); - } - ApiEncodingFormat::YAML => ( - Body::from(serde_yaml::to_string(&self.value).map_err(|e| { - ApiError::ServerError(format!( - "Unable to serialize response body as YAML: {:?}", - e - )) - })?), - "application/yaml", - ), - }; - - Response::builder() - .status(StatusCode::OK) - .header("content-type", content_type) - .body(body) - .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))) - } -} diff --git a/common/rest_types/src/lib.rs b/common/rest_types/src/lib.rs deleted file mode 100644 index 1bedd1cad..000000000 --- a/common/rest_types/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! A collection of types used to pass data across the rest HTTP API. -//! -//! This is primarily used by the validator client and the beacon node rest API. - -mod api_error; -mod beacon; -mod consensus; -mod handler; -mod node; -mod validator; - -pub use api_error::{ApiError, ApiResult}; -pub use beacon::{ - BlockResponse, CanonicalHeadResponse, Committee, HeadBeaconBlock, StateResponse, - ValidatorRequest, ValidatorResponse, -}; -pub use consensus::{IndividualVote, IndividualVotesRequest, IndividualVotesResponse}; -pub use handler::{ApiEncodingFormat, Handler}; -pub use node::{Health, SyncingResponse, SyncingStatus}; -pub use validator::{ - ValidatorDutiesRequest, ValidatorDuty, ValidatorDutyBytes, ValidatorSubscription, -}; diff --git a/common/rest_types/src/node.rs b/common/rest_types/src/node.rs deleted file mode 100644 index ca98645cc..000000000 --- a/common/rest_types/src/node.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Collection of types for the /node HTTP -use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use types::Slot; - -#[cfg(target_os = "linux")] -use {procinfo::pid, psutil::process::Process}; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -/// The current syncing status of the node. -pub struct SyncingStatus { - /// The starting slot of sync. - /// - /// For a finalized sync, this is the start slot of the current finalized syncing - /// chain. - /// - /// For head sync this is the last finalized slot. - pub starting_slot: Slot, - /// The current slot. - pub current_slot: Slot, - /// The highest known slot. For the current syncing chain. - /// - /// For a finalized sync, the target finalized slot. - /// For head sync, this is the highest known slot of all head chains. - pub highest_slot: Slot, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] -/// The response for the /node/syncing HTTP GET. -pub struct SyncingResponse { - /// Is the node syncing. - pub is_syncing: bool, - /// The current sync status. - pub sync_status: SyncingStatus, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -/// Reports on the health of the Lighthouse instance. -pub struct Health { - /// The pid of this process. - pub pid: u32, - /// The number of threads used by this pid. - pub pid_num_threads: i32, - /// The total resident memory used by this pid. - pub pid_mem_resident_set_size: u64, - /// The total virtual memory used by this pid. - pub pid_mem_virtual_memory_size: u64, - /// Total virtual memory on the system - pub sys_virt_mem_total: u64, - /// Total virtual memory available for new processes. - pub sys_virt_mem_available: u64, - /// Total virtual memory used on the system - pub sys_virt_mem_used: u64, - /// Total virtual memory not used on the system - pub sys_virt_mem_free: u64, - /// Percentage of virtual memory used on the system - pub sys_virt_mem_percent: f32, - /// System load average over 1 minute. - pub sys_loadavg_1: f64, - /// System load average over 5 minutes. - pub sys_loadavg_5: f64, - /// System load average over 15 minutes. - pub sys_loadavg_15: f64, -} - -impl Health { - #[cfg(not(target_os = "linux"))] - pub fn observe() -> Result<Self, String> { - Err("Health is only available on Linux".into()) - } - - #[cfg(target_os = "linux")] - pub fn observe() -> Result<Self, String> { - let process = - Process::current().map_err(|e| format!("Unable to get current process: {:?}", e))?; - - let process_mem = process - .memory_info() - .map_err(|e| format!("Unable to get process memory info: {:?}", e))?; - - let stat = pid::stat_self().map_err(|e| format!("Unable to get stat: {:?}", e))?; - - let vm = psutil::memory::virtual_memory() - .map_err(|e| format!("Unable to get virtual memory: {:?}", e))?; - let loadavg = - psutil::host::loadavg().map_err(|e| format!("Unable to get loadavg: {:?}", e))?; - - Ok(Self { - pid: process.pid(), - pid_num_threads: stat.num_threads, - pid_mem_resident_set_size: process_mem.rss(), - pid_mem_virtual_memory_size: process_mem.vms(), - sys_virt_mem_total: vm.total(), - sys_virt_mem_available: vm.available(), - sys_virt_mem_used: vm.used(), - sys_virt_mem_free: vm.free(), - sys_virt_mem_percent: vm.percent(), - sys_loadavg_1: loadavg.one, - sys_loadavg_5: loadavg.five, - sys_loadavg_15: loadavg.fifteen, - }) - } -} diff --git a/common/rest_types/src/validator.rs b/common/rest_types/src/validator.rs deleted file mode 100644 index 2b0f07729..000000000 --- a/common/rest_types/src/validator.rs +++ /dev/null @@ -1,103 +0,0 @@ -use bls::{PublicKey, PublicKeyBytes}; -use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use types::{CommitteeIndex, Epoch, Slot}; - -/// A Validator duty with the validator public key represented a `PublicKeyBytes`. -pub type ValidatorDutyBytes = ValidatorDutyBase<PublicKeyBytes>; -/// A validator duty with the pubkey represented as a `PublicKey`. -pub type ValidatorDuty = ValidatorDutyBase<PublicKey>; - -// NOTE: if you add or remove fields, please adjust `eq_ignoring_proposal_slots` -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] -pub struct ValidatorDutyBase<T> { - /// The validator's BLS public key, uniquely identifying them. - pub validator_pubkey: T, - /// The validator's index in `state.validators` - pub validator_index: Option<u64>, - /// The slot at which the validator must attest. - pub attestation_slot: Option<Slot>, - /// The index of the committee within `slot` of which the validator is a member. - pub attestation_committee_index: Option<CommitteeIndex>, - /// The position of the validator in the committee. - pub attestation_committee_position: Option<usize>, - /// The committee count at `attestation_slot`. - pub committee_count_at_slot: Option<u64>, - /// The slots in which a validator must propose a block (can be empty). - /// - /// Should be set to `None` when duties are not yet known (before the current epoch). - pub block_proposal_slots: Option<Vec<Slot>>, - /// This provides the modulo: `max(1, len(committee) // TARGET_AGGREGATORS_PER_COMMITTEE)` - /// which allows the validator client to determine if this duty requires the validator to be - /// aggregate attestations. - pub aggregator_modulo: Option<u64>, -} - -impl<T> ValidatorDutyBase<T> { - /// Return `true` if these validator duties are equal, ignoring their `block_proposal_slots`. - pub fn eq_ignoring_proposal_slots(&self, other: &Self) -> bool - where - T: PartialEq, - { - self.validator_pubkey == other.validator_pubkey - && self.validator_index == other.validator_index - && self.attestation_slot == other.attestation_slot - && self.attestation_committee_index == other.attestation_committee_index - && self.attestation_committee_position == other.attestation_committee_position - && self.committee_count_at_slot == other.committee_count_at_slot - && self.aggregator_modulo == other.aggregator_modulo - } -} - -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] -pub struct ValidatorDutiesRequest { - pub epoch: Epoch, - pub pubkeys: Vec<PublicKeyBytes>, -} - -/// A validator subscription, created when a validator subscribes to a slot to perform optional aggregation -/// duties. -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] -pub struct ValidatorSubscription { - /// The validators index. - pub validator_index: u64, - /// The index of the committee within `slot` of which the validator is a member. Used by the - /// beacon node to quickly evaluate the associated `SubnetId`. - pub attestation_committee_index: CommitteeIndex, - /// The slot in which to subscribe. - pub slot: Slot, - /// Committee count at slot to subscribe. - pub committee_count_at_slot: u64, - /// If true, the validator is an aggregator and the beacon node should aggregate attestations - /// for this slot. - pub is_aggregator: bool, -} - -#[cfg(test)] -mod test { - use super::*; - use bls::SecretKey; - - #[test] - fn eq_ignoring_proposal_slots() { - let validator_pubkey = SecretKey::deserialize(&[1; 32]).unwrap().public_key(); - - let duty1 = ValidatorDuty { - validator_pubkey, - validator_index: Some(10), - attestation_slot: Some(Slot::new(50)), - attestation_committee_index: Some(2), - attestation_committee_position: Some(6), - committee_count_at_slot: Some(4), - block_proposal_slots: None, - aggregator_modulo: Some(99), - }; - let duty2 = ValidatorDuty { - block_proposal_slots: Some(vec![Slot::new(42), Slot::new(45)]), - ..duty1.clone() - }; - assert_ne!(duty1, duty2); - assert!(duty1.eq_ignoring_proposal_slots(&duty2)); - assert!(duty2.eq_ignoring_proposal_slots(&duty1)); - } -} diff --git a/common/slot_clock/src/lib.rs b/common/slot_clock/src/lib.rs index 41c847498..0fe1bedfe 100644 --- a/common/slot_clock/src/lib.rs +++ b/common/slot_clock/src/lib.rs @@ -24,6 +24,16 @@ pub trait SlotClock: Send + Sync + Sized { /// Returns the slot at this present time. fn now(&self) -> Option<Slot>; + /// Returns the slot at this present time if genesis has happened. Otherwise, returns the + /// genesis slot. Returns `None` if there is an error reading the clock. + fn now_or_genesis(&self) -> Option<Slot> { + if self.is_prior_to_genesis()? { + Some(self.genesis_slot()) + } else { + self.now() + } + } + /// Indicates if the current time is prior to genesis time. /// /// Returns `None` if the system clock cannot be read. diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml new file mode 100644 index 000000000..98ddab5d8 --- /dev/null +++ b/common/warp_utils/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "warp_utils" +version = "0.1.0" +authors = ["Paul Hauner <paul@paulhauner.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +warp = "0.2.5" +eth2 = { path = "../eth2" } +types = { path = "../../consensus/types" } +beacon_chain = { path = "../../beacon_node/beacon_chain" } +state_processing = { path = "../../consensus/state_processing" } +safe_arith = { path = "../../consensus/safe_arith" } diff --git a/common/warp_utils/src/lib.rs b/common/warp_utils/src/lib.rs new file mode 100644 index 000000000..ec9cf3c34 --- /dev/null +++ b/common/warp_utils/src/lib.rs @@ -0,0 +1,5 @@ +//! This crate contains functions that are common across multiple `warp` HTTP servers in the +//! Lighthouse project. E.g., the `http_api` and `http_metrics` crates. + +pub mod reject; +pub mod reply; diff --git a/common/warp_utils/src/reject.rs b/common/warp_utils/src/reject.rs new file mode 100644 index 000000000..1243d5f68 --- /dev/null +++ b/common/warp_utils/src/reject.rs @@ -0,0 +1,168 @@ +use eth2::types::ErrorMessage; +use std::convert::Infallible; +use warp::{http::StatusCode, reject::Reject}; + +#[derive(Debug)] +pub struct BeaconChainError(pub beacon_chain::BeaconChainError); + +impl Reject for BeaconChainError {} + +pub fn beacon_chain_error(e: beacon_chain::BeaconChainError) -> warp::reject::Rejection { + warp::reject::custom(BeaconChainError(e)) +} + +#[derive(Debug)] +pub struct BeaconStateError(pub types::BeaconStateError); + +impl Reject for BeaconStateError {} + +pub fn beacon_state_error(e: types::BeaconStateError) -> warp::reject::Rejection { + warp::reject::custom(BeaconStateError(e)) +} + +#[derive(Debug)] +pub struct ArithError(pub safe_arith::ArithError); + +impl Reject for ArithError {} + +pub fn arith_error(e: safe_arith::ArithError) -> warp::reject::Rejection { + warp::reject::custom(ArithError(e)) +} + +#[derive(Debug)] +pub struct SlotProcessingError(pub state_processing::SlotProcessingError); + +impl Reject for SlotProcessingError {} + +pub fn slot_processing_error(e: state_processing::SlotProcessingError) -> warp::reject::Rejection { + warp::reject::custom(SlotProcessingError(e)) +} + +#[derive(Debug)] +pub struct BlockProductionError(pub beacon_chain::BlockProductionError); + +impl Reject for BlockProductionError {} + +pub fn block_production_error(e: beacon_chain::BlockProductionError) -> warp::reject::Rejection { + warp::reject::custom(BlockProductionError(e)) +} + +#[derive(Debug)] +pub struct CustomNotFound(pub String); + +impl Reject for CustomNotFound {} + +pub fn custom_not_found(msg: String) -> warp::reject::Rejection { + warp::reject::custom(CustomNotFound(msg)) +} + +#[derive(Debug)] +pub struct CustomBadRequest(pub String); + +impl Reject for CustomBadRequest {} + +pub fn custom_bad_request(msg: String) -> warp::reject::Rejection { + warp::reject::custom(CustomBadRequest(msg)) +} + +#[derive(Debug)] +pub struct CustomServerError(pub String); + +impl Reject for CustomServerError {} + +pub fn custom_server_error(msg: String) -> warp::reject::Rejection { + warp::reject::custom(CustomServerError(msg)) +} + +#[derive(Debug)] +pub struct BroadcastWithoutImport(pub String); + +impl Reject for BroadcastWithoutImport {} + +pub fn broadcast_without_import(msg: String) -> warp::reject::Rejection { + warp::reject::custom(BroadcastWithoutImport(msg)) +} + +#[derive(Debug)] +pub struct ObjectInvalid(pub String); + +impl Reject for ObjectInvalid {} + +pub fn object_invalid(msg: String) -> warp::reject::Rejection { + warp::reject::custom(ObjectInvalid(msg)) +} + +#[derive(Debug)] +pub struct NotSynced(pub String); + +impl Reject for NotSynced {} + +pub fn not_synced(msg: String) -> warp::reject::Rejection { + warp::reject::custom(NotSynced(msg)) +} + +/// This function receives a `Rejection` and tries to return a custom +/// value, otherwise simply passes the rejection along. +pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply, Infallible> { + let code; + let message; + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "NOT_FOUND".to_string(); + } else if let Some(e) = err.find::<warp::filters::body::BodyDeserializeError>() { + message = format!("BAD_REQUEST: body deserialize error: {}", e); + code = StatusCode::BAD_REQUEST; + } else if let Some(e) = err.find::<warp::reject::InvalidQuery>() { + code = StatusCode::BAD_REQUEST; + message = format!("BAD_REQUEST: invalid query: {}", e); + } else if let Some(e) = err.find::<crate::reject::BeaconChainError>() { + code = StatusCode::INTERNAL_SERVER_ERROR; + message = format!("UNHANDLED_ERROR: {:?}", e.0); + } else if let Some(e) = err.find::<crate::reject::BeaconStateError>() { + code = StatusCode::INTERNAL_SERVER_ERROR; + message = format!("UNHANDLED_ERROR: {:?}", e.0); + } else if let Some(e) = err.find::<crate::reject::SlotProcessingError>() { + code = StatusCode::INTERNAL_SERVER_ERROR; + message = format!("UNHANDLED_ERROR: {:?}", e.0); + } else if let Some(e) = err.find::<crate::reject::BlockProductionError>() { + code = StatusCode::INTERNAL_SERVER_ERROR; + message = format!("UNHANDLED_ERROR: {:?}", e.0); + } else if let Some(e) = err.find::<crate::reject::CustomNotFound>() { + code = StatusCode::NOT_FOUND; + message = format!("NOT_FOUND: {}", e.0); + } else if let Some(e) = err.find::<crate::reject::CustomBadRequest>() { + code = StatusCode::BAD_REQUEST; + message = format!("BAD_REQUEST: {}", e.0); + } else if let Some(e) = err.find::<crate::reject::CustomServerError>() { + code = StatusCode::INTERNAL_SERVER_ERROR; + message = format!("INTERNAL_SERVER_ERROR: {}", e.0); + } else if let Some(e) = err.find::<crate::reject::BroadcastWithoutImport>() { + code = StatusCode::ACCEPTED; + message = format!( + "ACCEPTED: the object was broadcast to the network without being \ + fully imported to the local database: {}", + e.0 + ); + } else if let Some(e) = err.find::<crate::reject::ObjectInvalid>() { + code = StatusCode::BAD_REQUEST; + message = format!("BAD_REQUEST: Invalid object: {}", e.0); + } else if let Some(e) = err.find::<crate::reject::NotSynced>() { + code = StatusCode::SERVICE_UNAVAILABLE; + message = format!("SERVICE_UNAVAILABLE: beacon node is syncing: {}", e.0); + } else if err.find::<warp::reject::MethodNotAllowed>().is_some() { + code = StatusCode::METHOD_NOT_ALLOWED; + message = "METHOD_NOT_ALLOWED".to_string(); + } else { + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "UNHANDLED_REJECTION".to_string(); + } + + let json = warp::reply::json(&ErrorMessage { + code: code.as_u16(), + message, + stacktraces: vec![], + }); + + Ok(warp::reply::with_status(json, code)) +} diff --git a/common/warp_utils/src/reply.rs b/common/warp_utils/src/reply.rs new file mode 100644 index 000000000..dcec6214f --- /dev/null +++ b/common/warp_utils/src/reply.rs @@ -0,0 +1,15 @@ +/// Add CORS headers to `reply` only if `allow_origin.is_some()`. +pub fn maybe_cors<T: warp::Reply + 'static>( + reply: T, + allow_origin: Option<&String>, +) -> Box<dyn warp::Reply> { + if let Some(allow_origin) = allow_origin { + Box::new(warp::reply::with_header( + reply, + "Access-Control-Allow-Origin", + allow_origin, + )) + } else { + Box::new(reply) + } +} diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 99f998e55..f6c43ae42 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -4,7 +4,7 @@ use proto_array::{Block as ProtoBlock, ProtoArrayForkChoice}; use ssz_derive::{Decode, Encode}; use types::{ BeaconBlock, BeaconState, BeaconStateError, Checkpoint, Epoch, EthSpec, Hash256, - IndexedAttestation, Slot, + IndexedAttestation, RelativeEpoch, ShufflingId, Slot, }; use crate::ForkChoiceStore; @@ -240,10 +240,18 @@ where /// Instantiates `Self` from the genesis parameters. pub fn from_genesis( fc_store: T, + genesis_block_root: Hash256, genesis_block: &BeaconBlock<E>, + genesis_state: &BeaconState<E>, ) -> Result<Self, Error<T::Error>> { let finalized_block_slot = genesis_block.slot; let finalized_block_state_root = genesis_block.state_root; + let current_epoch_shuffling_id = + ShufflingId::new(genesis_block_root, genesis_state, RelativeEpoch::Current) + .map_err(Error::BeaconStateError)?; + let next_epoch_shuffling_id = + ShufflingId::new(genesis_block_root, genesis_state, RelativeEpoch::Next) + .map_err(Error::BeaconStateError)?; let proto_array = ProtoArrayForkChoice::new( finalized_block_slot, @@ -251,6 +259,8 @@ where fc_store.justified_checkpoint().epoch, fc_store.finalized_checkpoint().epoch, fc_store.finalized_checkpoint().root, + current_epoch_shuffling_id, + next_epoch_shuffling_id, )?; Ok(Self { @@ -534,6 +544,10 @@ where root: block_root, parent_root: Some(block.parent_root), target_root, + current_epoch_shuffling_id: ShufflingId::new(block_root, state, RelativeEpoch::Current) + .map_err(Error::BeaconStateError)?, + next_epoch_shuffling_id: ShufflingId::new(block_root, state, RelativeEpoch::Next) + .map_err(Error::BeaconStateError)?, state_root: block.state_root, justified_epoch: state.current_justified_checkpoint.epoch, finalized_epoch: state.finalized_checkpoint.epoch, diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 78c7534cd..7b508afd4 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -6,3 +6,4 @@ pub use crate::fork_choice::{ SAFE_SLOTS_TO_UPDATE_JUSTIFIED, }; pub use fork_choice_store::ForkChoiceStore; +pub use proto_array::Block as ProtoBlock; diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index ffa9cbe6b..86fbbd8ec 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -351,7 +351,7 @@ impl ForkChoiceTest { let mut verified_attestation = self .harness .chain - .verify_unaggregated_attestation_for_gossip(attestation, subnet_id) + .verify_unaggregated_attestation_for_gossip(attestation, Some(subnet_id)) .expect("precondition: should gossip verify attestation"); if let MutationDelay::Blocks(slots) = delay { diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 6e1bd970b..9cac0bafb 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -4,7 +4,7 @@ mod votes; use crate::proto_array_fork_choice::{Block, ProtoArrayForkChoice}; use serde_derive::{Deserialize, Serialize}; -use types::{Epoch, Hash256, Slot}; +use types::{Epoch, Hash256, ShufflingId, Slot}; pub use ffg_updates::*; pub use no_votes::*; @@ -55,12 +55,15 @@ pub struct ForkChoiceTestDefinition { impl ForkChoiceTestDefinition { pub fn run(self) { + let junk_shuffling_id = ShufflingId::from_components(Epoch::new(0), Hash256::zero()); let mut fork_choice = ProtoArrayForkChoice::new( self.finalized_block_slot, Hash256::zero(), self.justified_epoch, self.finalized_epoch, self.finalized_root, + junk_shuffling_id.clone(), + junk_shuffling_id, ) .expect("should create fork choice struct"); @@ -125,6 +128,14 @@ impl ForkChoiceTestDefinition { parent_root: Some(parent_root), state_root: Hash256::zero(), target_root: Hash256::zero(), + current_epoch_shuffling_id: ShufflingId::from_components( + Epoch::new(0), + Hash256::zero(), + ), + next_epoch_shuffling_id: ShufflingId::from_components( + Epoch::new(0), + Hash256::zero(), + ), justified_epoch, finalized_epoch, }; diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 18db8d340..c89a96628 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -2,7 +2,7 @@ use crate::{error::Error, Block}; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; -use types::{Epoch, Hash256, Slot}; +use types::{Epoch, Hash256, ShufflingId, Slot}; #[derive(Clone, PartialEq, Debug, Encode, Decode, Serialize, Deserialize)] pub struct ProtoNode { @@ -18,6 +18,8 @@ pub struct ProtoNode { /// The `target_root` is not necessary for `ProtoArray` either, it also just exists for upstream /// components (namely fork choice attestation verification). pub target_root: Hash256, + pub current_epoch_shuffling_id: ShufflingId, + pub next_epoch_shuffling_id: ShufflingId, pub root: Hash256, pub parent: Option<usize>, pub justified_epoch: Epoch, @@ -142,6 +144,8 @@ impl ProtoArray { slot: block.slot, root: block.root, target_root: block.target_root, + current_epoch_shuffling_id: block.current_epoch_shuffling_id, + next_epoch_shuffling_id: block.next_epoch_shuffling_id, state_root: block.state_root, parent: block .parent_root diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 451f39993..e4cf5bbc6 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -4,7 +4,7 @@ use crate::ssz_container::SszContainer; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; -use types::{Epoch, Hash256, Slot}; +use types::{Epoch, Hash256, ShufflingId, Slot}; pub const DEFAULT_PRUNE_THRESHOLD: usize = 256; @@ -25,6 +25,8 @@ pub struct Block { pub parent_root: Option<Hash256>, pub state_root: Hash256, pub target_root: Hash256, + pub current_epoch_shuffling_id: ShufflingId, + pub next_epoch_shuffling_id: ShufflingId, pub justified_epoch: Epoch, pub finalized_epoch: Epoch, } @@ -70,6 +72,8 @@ impl ProtoArrayForkChoice { justified_epoch: Epoch, finalized_epoch: Epoch, finalized_root: Hash256, + current_epoch_shuffling_id: ShufflingId, + next_epoch_shuffling_id: ShufflingId, ) -> Result<Self, String> { let mut proto_array = ProtoArray { prune_threshold: DEFAULT_PRUNE_THRESHOLD, @@ -87,6 +91,8 @@ impl ProtoArrayForkChoice { // We are using the finalized_root as the target_root, since it always lies on an // epoch boundary. target_root: finalized_root, + current_epoch_shuffling_id, + next_epoch_shuffling_id, justified_epoch, finalized_epoch, }; @@ -194,6 +200,8 @@ impl ProtoArrayForkChoice { parent_root, state_root: block.state_root, target_root: block.target_root, + current_epoch_shuffling_id: block.current_epoch_shuffling_id.clone(), + next_epoch_shuffling_id: block.next_epoch_shuffling_id.clone(), justified_epoch: block.justified_epoch, finalized_epoch: block.finalized_epoch, }) @@ -341,6 +349,7 @@ mod test_compute_deltas { let finalized_desc = Hash256::from_low_u64_be(2); let not_finalized_desc = Hash256::from_low_u64_be(3); let unknown = Hash256::from_low_u64_be(4); + let junk_shuffling_id = ShufflingId::from_components(Epoch::new(0), Hash256::zero()); let mut fc = ProtoArrayForkChoice::new( genesis_slot, @@ -348,6 +357,8 @@ mod test_compute_deltas { genesis_epoch, genesis_epoch, finalized_root, + junk_shuffling_id.clone(), + junk_shuffling_id.clone(), ) .unwrap(); @@ -359,6 +370,8 @@ mod test_compute_deltas { parent_root: Some(finalized_root), state_root, target_root: finalized_root, + current_epoch_shuffling_id: junk_shuffling_id.clone(), + next_epoch_shuffling_id: junk_shuffling_id.clone(), justified_epoch: genesis_epoch, finalized_epoch: genesis_epoch, }) @@ -372,6 +385,8 @@ mod test_compute_deltas { parent_root: None, state_root, target_root: finalized_root, + current_epoch_shuffling_id: junk_shuffling_id.clone(), + next_epoch_shuffling_id: junk_shuffling_id.clone(), justified_epoch: genesis_epoch, finalized_epoch: genesis_epoch, }) diff --git a/consensus/serde_hex/Cargo.toml b/consensus/serde_hex/Cargo.toml deleted file mode 100644 index 2df5ff02a..000000000 --- a/consensus/serde_hex/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "serde_hex" -version = "0.2.0" -authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2018" - -[dependencies] -serde = "1.0.110" -hex = "0.4.2" diff --git a/consensus/serde_utils/Cargo.toml b/consensus/serde_utils/Cargo.toml index 1fb35736b..8c0013562 100644 --- a/consensus/serde_utils/Cargo.toml +++ b/consensus/serde_utils/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] serde = { version = "1.0.110", features = ["derive"] } serde_derive = "1.0.110" +hex = "0.4.2" [dev-dependencies] serde_json = "1.0.52" diff --git a/consensus/serde_utils/src/bytes_4_hex.rs b/consensus/serde_utils/src/bytes_4_hex.rs new file mode 100644 index 000000000..e057d1a12 --- /dev/null +++ b/consensus/serde_utils/src/bytes_4_hex.rs @@ -0,0 +1,38 @@ +//! Formats `[u8; 4]` as a 0x-prefixed hex string. +//! +//! E.g., `[0, 1, 2, 3]` serializes as `"0x00010203"`. + +use crate::hex::PrefixedHexVisitor; +use serde::de::Error; +use serde::{Deserializer, Serializer}; + +const BYTES_LEN: usize = 4; + +pub fn serialize<S>(bytes: &[u8; BYTES_LEN], serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, +{ + let mut hex_string: String = "0x".to_string(); + hex_string.push_str(&hex::encode(&bytes)); + + serializer.serialize_str(&hex_string) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; BYTES_LEN], D::Error> +where + D: Deserializer<'de>, +{ + let decoded = deserializer.deserialize_str(PrefixedHexVisitor)?; + + if decoded.len() != BYTES_LEN { + return Err(D::Error::custom(format!( + "expected {} bytes for array, got {}", + BYTES_LEN, + decoded.len() + ))); + } + + let mut array = [0; BYTES_LEN]; + array.copy_from_slice(&decoded); + Ok(array) +} diff --git a/consensus/serde_hex/src/lib.rs b/consensus/serde_utils/src/hex.rs similarity index 81% rename from consensus/serde_hex/src/lib.rs rename to consensus/serde_utils/src/hex.rs index db8422275..79dfaa506 100644 --- a/consensus/serde_hex/src/lib.rs +++ b/consensus/serde_utils/src/hex.rs @@ -1,6 +1,9 @@ +//! Provides utilities for parsing 0x-prefixed hex strings. + use serde::de::{self, Visitor}; use std::fmt; +/// Encode `data` as a 0x-prefixed hex string. pub fn encode<T: AsRef<[u8]>>(data: T) -> String { let hex = hex::encode(data); let mut s = "0x".to_string(); @@ -8,6 +11,15 @@ pub fn encode<T: AsRef<[u8]>>(data: T) -> String { s } +/// Decode `data` from a 0x-prefixed hex string. +pub fn decode(s: &str) -> Result<Vec<u8>, String> { + if s.starts_with("0x") { + hex::decode(&s[2..]).map_err(|e| format!("invalid hex: {:?}", e)) + } else { + Err("hex must have 0x prefix".to_string()) + } +} + pub struct PrefixedHexVisitor; impl<'de> Visitor<'de> for PrefixedHexVisitor { diff --git a/consensus/serde_utils/src/lib.rs b/consensus/serde_utils/src/lib.rs index df2b44b62..0016e67a3 100644 --- a/consensus/serde_utils/src/lib.rs +++ b/consensus/serde_utils/src/lib.rs @@ -1,2 +1,9 @@ -pub mod quoted_u64; +mod quoted_int; + +pub mod bytes_4_hex; +pub mod hex; pub mod quoted_u64_vec; +pub mod u32_hex; +pub mod u8_hex; + +pub use quoted_int::{quoted_u32, quoted_u64, quoted_u8}; diff --git a/consensus/serde_utils/src/quoted_int.rs b/consensus/serde_utils/src/quoted_int.rs new file mode 100644 index 000000000..24edf1ebe --- /dev/null +++ b/consensus/serde_utils/src/quoted_int.rs @@ -0,0 +1,144 @@ +//! Formats some integer types using quotes. +//! +//! E.g., `1` serializes as `"1"`. +//! +//! Quotes can be optional during decoding. + +use serde::{Deserializer, Serializer}; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::marker::PhantomData; + +macro_rules! define_mod { + ($int: ty, $visit_fn: ident) => { + /// Serde support for deserializing quoted integers. + /// + /// Configurable so that quotes are either required or optional. + pub struct QuotedIntVisitor<T> { + require_quotes: bool, + _phantom: PhantomData<T>, + } + + impl<'a, T> serde::de::Visitor<'a> for QuotedIntVisitor<T> + where + T: From<$int> + Into<$int> + Copy + TryFrom<u64>, + { + type Value = T; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.require_quotes { + write!(formatter, "a quoted integer") + } else { + write!(formatter, "a quoted or unquoted integer") + } + } + + fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + s.parse::<$int>() + .map(T::from) + .map_err(serde::de::Error::custom) + } + + fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + if self.require_quotes { + Err(serde::de::Error::custom( + "received unquoted integer when quotes are required", + )) + } else { + T::try_from(v).map_err(|_| serde::de::Error::custom("invalid integer")) + } + } + } + + /// Wrapper type for requiring quotes on a `$int`-like type. + /// + /// Unlike using `serde(with = "quoted_$int::require_quotes")` this is composable, and can be nested + /// inside types like `Option`, `Result` and `Vec`. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] + #[serde(transparent)] + pub struct Quoted<T> + where + T: From<$int> + Into<$int> + Copy + TryFrom<u64>, + { + #[serde(with = "require_quotes")] + pub value: T, + } + + /// Serialize with quotes. + pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + T: From<$int> + Into<$int> + Copy, + { + let v: $int = (*value).into(); + serializer.serialize_str(&format!("{}", v)) + } + + /// Deserialize with or without quotes. + pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error> + where + D: Deserializer<'de>, + T: From<$int> + Into<$int> + Copy + TryFrom<u64>, + { + deserializer.deserialize_any(QuotedIntVisitor { + require_quotes: false, + _phantom: PhantomData, + }) + } + + /// Requires quotes when deserializing. + /// + /// Usage: `#[serde(with = "quoted_u64::require_quotes")]`. + pub mod require_quotes { + pub use super::serialize; + use super::*; + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error> + where + D: Deserializer<'de>, + T: From<$int> + Into<$int> + Copy + TryFrom<u64>, + { + deserializer.deserialize_any(QuotedIntVisitor { + require_quotes: true, + _phantom: PhantomData, + }) + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn require_quotes() { + let x = serde_json::from_str::<Quoted<$int>>("\"8\"").unwrap(); + assert_eq!(x.value, 8); + serde_json::from_str::<Quoted<$int>>("8").unwrap_err(); + } + } + }; +} + +pub mod quoted_u8 { + use super::*; + + define_mod!(u8, visit_u8); +} + +pub mod quoted_u32 { + use super::*; + + define_mod!(u32, visit_u32); +} + +pub mod quoted_u64 { + use super::*; + + define_mod!(u64, visit_u64); +} diff --git a/consensus/serde_utils/src/quoted_u64.rs b/consensus/serde_utils/src/quoted_u64.rs deleted file mode 100644 index 2e73a104f..000000000 --- a/consensus/serde_utils/src/quoted_u64.rs +++ /dev/null @@ -1,115 +0,0 @@ -use serde::{Deserializer, Serializer}; -use serde_derive::{Deserialize, Serialize}; -use std::marker::PhantomData; - -/// Serde support for deserializing quoted integers. -/// -/// Configurable so that quotes are either required or optional. -pub struct QuotedIntVisitor<T> { - require_quotes: bool, - _phantom: PhantomData<T>, -} - -impl<'a, T> serde::de::Visitor<'a> for QuotedIntVisitor<T> -where - T: From<u64> + Into<u64> + Copy, -{ - type Value = T; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - if self.require_quotes { - write!(formatter, "a quoted integer") - } else { - write!(formatter, "a quoted or unquoted integer") - } - } - - fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> - where - E: serde::de::Error, - { - s.parse::<u64>() - .map(T::from) - .map_err(serde::de::Error::custom) - } - - fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> - where - E: serde::de::Error, - { - if self.require_quotes { - Err(serde::de::Error::custom( - "received unquoted integer when quotes are required", - )) - } else { - Ok(T::from(v)) - } - } -} - -/// Wrapper type for requiring quotes on a `u64`-like type. -/// -/// Unlike using `serde(with = "quoted_u64::require_quotes")` this is composable, and can be nested -/// inside types like `Option`, `Result` and `Vec`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] -#[serde(transparent)] -pub struct Quoted<T> -where - T: From<u64> + Into<u64> + Copy, -{ - #[serde(with = "require_quotes")] - pub value: T, -} - -/// Serialize with quotes. -pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error> -where - S: Serializer, - T: From<u64> + Into<u64> + Copy, -{ - let v: u64 = (*value).into(); - serializer.serialize_str(&format!("{}", v)) -} - -/// Deserialize with or without quotes. -pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error> -where - D: Deserializer<'de>, - T: From<u64> + Into<u64> + Copy, -{ - deserializer.deserialize_any(QuotedIntVisitor { - require_quotes: false, - _phantom: PhantomData, - }) -} - -/// Requires quotes when deserializing. -/// -/// Usage: `#[serde(with = "quoted_u64::require_quotes")]`. -pub mod require_quotes { - pub use super::serialize; - use super::*; - - pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error> - where - D: Deserializer<'de>, - T: From<u64> + Into<u64> + Copy, - { - deserializer.deserialize_any(QuotedIntVisitor { - require_quotes: true, - _phantom: PhantomData, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn require_quotes() { - let x = serde_json::from_str::<Quoted<u64>>("\"8\"").unwrap(); - assert_eq!(x.value, 8); - serde_json::from_str::<Quoted<u64>>("8").unwrap_err(); - } -} diff --git a/consensus/serde_utils/src/quoted_u64_vec.rs b/consensus/serde_utils/src/quoted_u64_vec.rs index c5badee50..f124c9890 100644 --- a/consensus/serde_utils/src/quoted_u64_vec.rs +++ b/consensus/serde_utils/src/quoted_u64_vec.rs @@ -1,3 +1,9 @@ +//! Formats `Vec<u64>` using quotes. +//! +//! E.g., `vec![0, 1, 2]` serializes as `["0", "1", "2"]`. +//! +//! Quotes can be optional during decoding. + use serde::ser::SerializeSeq; use serde::{Deserializer, Serializer}; use serde_derive::{Deserialize, Serialize}; @@ -6,7 +12,7 @@ use serde_derive::{Deserialize, Serialize}; #[serde(transparent)] pub struct QuotedIntWrapper { #[serde(with = "crate::quoted_u64")] - int: u64, + pub int: u64, } pub struct QuotedIntVecVisitor; diff --git a/consensus/serde_utils/src/u32_hex.rs b/consensus/serde_utils/src/u32_hex.rs new file mode 100644 index 000000000..c1ab3537b --- /dev/null +++ b/consensus/serde_utils/src/u32_hex.rs @@ -0,0 +1,21 @@ +//! Formats `u32` as a 0x-prefixed, little-endian hex string. +//! +//! E.g., `0` serializes as `"0x00000000"`. + +use crate::bytes_4_hex; +use serde::{Deserializer, Serializer}; + +pub fn serialize<S>(num: &u32, serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, +{ + let hex = format!("0x{}", hex::encode(num.to_le_bytes())); + serializer.serialize_str(&hex) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result<u32, D::Error> +where + D: Deserializer<'de>, +{ + bytes_4_hex::deserialize(deserializer).map(u32::from_le_bytes) +} diff --git a/consensus/serde_utils/src/u8_hex.rs b/consensus/serde_utils/src/u8_hex.rs new file mode 100644 index 000000000..8083e1d12 --- /dev/null +++ b/consensus/serde_utils/src/u8_hex.rs @@ -0,0 +1,29 @@ +//! Formats `u8` as a 0x-prefixed hex string. +//! +//! E.g., `0` serializes as `"0x00"`. + +use crate::hex::PrefixedHexVisitor; +use serde::de::Error; +use serde::{Deserializer, Serializer}; + +pub fn serialize<S>(byte: &u8, serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, +{ + let hex = format!("0x{}", hex::encode([*byte])); + serializer.serialize_str(&hex) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result<u8, D::Error> +where + D: Deserializer<'de>, +{ + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; + if bytes.len() != 1 { + return Err(D::Error::custom(format!( + "expected 1 byte for u8, got {}", + bytes.len() + ))); + } + Ok(bytes[0]) +} diff --git a/consensus/ssz_types/Cargo.toml b/consensus/ssz_types/Cargo.toml index 144b3ce31..ca6a5adbe 100644 --- a/consensus/ssz_types/Cargo.toml +++ b/consensus/ssz_types/Cargo.toml @@ -11,7 +11,7 @@ name = "ssz_types" tree_hash = "0.1.0" serde = "1.0.110" serde_derive = "1.0.110" -serde_hex = { path = "../serde_hex" } +serde_utils = { path = "../serde_utils" } eth2_ssz = "0.1.2" typenum = "1.12.0" arbitrary = { version = "0.4.4", features = ["derive"], optional = true } diff --git a/consensus/ssz_types/src/bitfield.rs b/consensus/ssz_types/src/bitfield.rs index 1b6dce3ec..09fa9fc2d 100644 --- a/consensus/ssz_types/src/bitfield.rs +++ b/consensus/ssz_types/src/bitfield.rs @@ -3,7 +3,7 @@ use crate::Error; use core::marker::PhantomData; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; +use serde_utils::hex::{encode as hex_encode, PrefixedHexVisitor}; use ssz::{Decode, Encode}; use tree_hash::Hash256; use typenum::Unsigned; diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 80b4007b9..c3a5cd90d 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -39,6 +39,8 @@ tempfile = "3.1.0" derivative = "2.1.1" rusqlite = { version = "0.23.1", features = ["bundled"], optional = true } arbitrary = { version = "0.4.4", features = ["derive"], optional = true } +serde_utils = { path = "../serde_utils" } +regex = "1.3.9" [dev-dependencies] serde_json = "1.0.52" diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/aggregate_and_proof.rs index 737c891c9..528712261 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/aggregate_and_proof.rs @@ -16,6 +16,7 @@ use tree_hash_derive::TreeHash; #[serde(bound = "T: EthSpec")] pub struct AggregateAndProof<T: EthSpec> { /// The index of the validator that created the attestation. + #[serde(with = "serde_utils::quoted_u64")] pub aggregator_index: u64, /// The aggregate attestation. pub aggregate: Attestation<T>, diff --git a/consensus/types/src/attestation_data.rs b/consensus/types/src/attestation_data.rs index 67fb28002..07fa529e0 100644 --- a/consensus/types/src/attestation_data.rs +++ b/consensus/types/src/attestation_data.rs @@ -26,6 +26,7 @@ use tree_hash_derive::TreeHash; )] pub struct AttestationData { pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] pub index: u64, // LMD GHOST vote diff --git a/consensus/types/src/attestation_duty.rs b/consensus/types/src/attestation_duty.rs index c32e4683e..613d7fd1c 100644 --- a/consensus/types/src/attestation_duty.rs +++ b/consensus/types/src/attestation_duty.rs @@ -12,4 +12,7 @@ pub struct AttestationDuty { pub committee_position: usize, /// The total number of attesters in the committee. pub committee_len: usize, + /// The committee count at `attestation_slot`. + #[serde(with = "serde_utils::quoted_u64")] + pub committees_at_slot: u64, } diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index eeb10458b..d3a916070 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -16,6 +16,7 @@ use tree_hash_derive::TreeHash; #[serde(bound = "T: EthSpec")] pub struct BeaconBlock<T: EthSpec> { pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] pub proposer_index: u64, pub parent_root: Hash256, pub state_root: Hash256, diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index 489c5bc9d..ef28307ed 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -1,5 +1,4 @@ use crate::test_utils::TestRandom; -use crate::utils::{graffiti_from_hex_str, graffiti_to_hex_str, Graffiti}; use crate::*; use serde_derive::{Deserialize, Serialize}; @@ -17,10 +16,6 @@ use tree_hash_derive::TreeHash; pub struct BeaconBlockBody<T: EthSpec> { pub randao_reveal: Signature, pub eth1_data: Eth1Data, - #[serde( - serialize_with = "graffiti_to_hex_str", - deserialize_with = "graffiti_from_hex_str" - )] pub graffiti: Graffiti, pub proposer_slashings: VariableList<ProposerSlashing, T::MaxProposerSlashings>, pub attester_slashings: VariableList<AttesterSlashing<T>, T::MaxAttesterSlashings>, diff --git a/consensus/types/src/beacon_block_header.rs b/consensus/types/src/beacon_block_header.rs index 04a20e56d..708c0e16f 100644 --- a/consensus/types/src/beacon_block_header.rs +++ b/consensus/types/src/beacon_block_header.rs @@ -14,6 +14,7 @@ use tree_hash_derive::TreeHash; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] pub struct BeaconBlockHeader { pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] pub proposer_index: u64, pub parent_root: Hash256, pub state_root: Hash256, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index a2d923da9..25cb85ce8 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -157,6 +157,7 @@ where T: EthSpec, { // Versioning + #[serde(with = "serde_utils::quoted_u64")] pub genesis_time: u64, pub genesis_validators_root: Hash256, pub slot: Slot, @@ -173,6 +174,7 @@ where // Ethereum 1.0 chain data pub eth1_data: Eth1Data, pub eth1_data_votes: VariableList<Eth1Data, T::SlotsPerEth1VotingPeriod>, + #[serde(with = "serde_utils::quoted_u64")] pub eth1_deposit_index: u64, // Registry @@ -913,6 +915,13 @@ impl<T: EthSpec> BeaconState<T> { self.exit_cache = ExitCache::default(); } + /// Returns `true` if the committee cache for `relative_epoch` is built and ready to use. + pub fn committee_cache_is_initialized(&self, relative_epoch: RelativeEpoch) -> bool { + let i = Self::committee_cache_index(relative_epoch); + + self.committee_caches[i].is_initialized_at(relative_epoch.into_epoch(self.current_epoch())) + } + /// Build an epoch cache, unless it is has already been built. pub fn build_committee_cache( &mut self, diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index 6ee24cd2b..728c9cf02 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -186,6 +186,7 @@ impl CommitteeCache { index, committee_position, committee_len, + committees_at_slot: self.committees_per_slot(), }) }) } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index c621acb81..7327895ee 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -4,10 +4,6 @@ use serde_derive::{Deserialize, Serialize}; use std::fs::File; use std::path::Path; use tree_hash::TreeHash; -use utils::{ - fork_from_hex_str, fork_to_hex_str, u32_from_hex_str, u32_to_hex_str, u8_from_hex_str, - u8_to_hex_str, -}; /// Each of the BLS signature domains. /// @@ -65,12 +61,9 @@ pub struct ChainSpec { /* * Initial Values */ - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] pub genesis_fork_version: [u8; 4], - #[serde(deserialize_with = "u8_from_hex_str", serialize_with = "u8_to_hex_str")] + #[serde(with = "serde_utils::u8_hex")] pub bls_withdrawal_prefix_byte: u8, /* @@ -115,6 +108,7 @@ pub struct ChainSpec { */ pub eth1_follow_distance: u64, pub seconds_per_eth1_block: u64, + pub deposit_contract_address: Address, /* * Networking @@ -326,6 +320,9 @@ impl ChainSpec { */ eth1_follow_distance: 1_024, seconds_per_eth1_block: 14, + deposit_contract_address: "1234567890123456789012345678901234567890" + .parse() + .expect("chain spec deposit contract address"), /* * Network specific @@ -448,104 +445,127 @@ pub struct YamlConfig { #[serde(default)] config_name: String, // ChainSpec - max_committees_per_slot: usize, - target_committee_size: usize, + #[serde(with = "serde_utils::quoted_u64")] + max_committees_per_slot: u64, + #[serde(with = "serde_utils::quoted_u64")] + target_committee_size: u64, + #[serde(with = "serde_utils::quoted_u64")] min_per_epoch_churn_limit: u64, + #[serde(with = "serde_utils::quoted_u64")] churn_limit_quotient: u64, + #[serde(with = "serde_utils::quoted_u8")] shuffle_round_count: u8, + #[serde(with = "serde_utils::quoted_u64")] min_genesis_active_validator_count: u64, + #[serde(with = "serde_utils::quoted_u64")] min_genesis_time: u64, + #[serde(with = "serde_utils::quoted_u64")] genesis_delay: u64, + #[serde(with = "serde_utils::quoted_u64")] min_deposit_amount: u64, + #[serde(with = "serde_utils::quoted_u64")] max_effective_balance: u64, + #[serde(with = "serde_utils::quoted_u64")] ejection_balance: u64, + #[serde(with = "serde_utils::quoted_u64")] effective_balance_increment: u64, + #[serde(with = "serde_utils::quoted_u64")] hysteresis_quotient: u64, + #[serde(with = "serde_utils::quoted_u64")] hysteresis_downward_multiplier: u64, + #[serde(with = "serde_utils::quoted_u64")] hysteresis_upward_multiplier: u64, // Proportional slashing multiplier defaults to 3 for compatibility with Altona and Medalla. #[serde(default = "default_proportional_slashing_multiplier")] + #[serde(with = "serde_utils::quoted_u64")] proportional_slashing_multiplier: u64, - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] genesis_fork_version: [u8; 4], - #[serde(deserialize_with = "u8_from_hex_str", serialize_with = "u8_to_hex_str")] + #[serde(with = "serde_utils::u8_hex")] bls_withdrawal_prefix: u8, + #[serde(with = "serde_utils::quoted_u64")] seconds_per_slot: u64, + #[serde(with = "serde_utils::quoted_u64")] min_attestation_inclusion_delay: u64, + #[serde(with = "serde_utils::quoted_u64")] min_seed_lookahead: u64, + #[serde(with = "serde_utils::quoted_u64")] max_seed_lookahead: u64, + #[serde(with = "serde_utils::quoted_u64")] min_epochs_to_inactivity_penalty: u64, + #[serde(with = "serde_utils::quoted_u64")] min_validator_withdrawability_delay: u64, + #[serde(with = "serde_utils::quoted_u64")] shard_committee_period: u64, + #[serde(with = "serde_utils::quoted_u64")] base_reward_factor: u64, + #[serde(with = "serde_utils::quoted_u64")] whistleblower_reward_quotient: u64, + #[serde(with = "serde_utils::quoted_u64")] proposer_reward_quotient: u64, + #[serde(with = "serde_utils::quoted_u64")] inactivity_penalty_quotient: u64, + #[serde(with = "serde_utils::quoted_u64")] min_slashing_penalty_quotient: u64, + #[serde(with = "serde_utils::quoted_u64")] safe_slots_to_update_justified: u64, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_beacon_proposer: u32, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_beacon_attester: u32, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_randao: u32, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_deposit: u32, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_voluntary_exit: u32, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_selection_proof: u32, - #[serde( - deserialize_with = "u32_from_hex_str", - serialize_with = "u32_to_hex_str" - )] + #[serde(with = "serde_utils::u32_hex")] domain_aggregate_and_proof: u32, // EthSpec + #[serde(with = "serde_utils::quoted_u32")] max_validators_per_committee: u32, + #[serde(with = "serde_utils::quoted_u64")] slots_per_epoch: u64, + #[serde(with = "serde_utils::quoted_u64")] epochs_per_eth1_voting_period: u64, - slots_per_historical_root: usize, - epochs_per_historical_vector: usize, - epochs_per_slashings_vector: usize, + #[serde(with = "serde_utils::quoted_u64")] + slots_per_historical_root: u64, + #[serde(with = "serde_utils::quoted_u64")] + epochs_per_historical_vector: u64, + #[serde(with = "serde_utils::quoted_u64")] + epochs_per_slashings_vector: u64, + #[serde(with = "serde_utils::quoted_u64")] historical_roots_limit: u64, + #[serde(with = "serde_utils::quoted_u64")] validator_registry_limit: u64, + #[serde(with = "serde_utils::quoted_u32")] max_proposer_slashings: u32, + #[serde(with = "serde_utils::quoted_u32")] max_attester_slashings: u32, + #[serde(with = "serde_utils::quoted_u32")] max_attestations: u32, + #[serde(with = "serde_utils::quoted_u32")] max_deposits: u32, + #[serde(with = "serde_utils::quoted_u32")] max_voluntary_exits: u32, // Validator + #[serde(with = "serde_utils::quoted_u64")] eth1_follow_distance: u64, + #[serde(with = "serde_utils::quoted_u64")] target_aggregators_per_committee: u64, + #[serde(with = "serde_utils::quoted_u64")] random_subnets_per_validator: u64, + #[serde(with = "serde_utils::quoted_u64")] epochs_per_random_subnet_subscription: u64, + #[serde(with = "serde_utils::quoted_u64")] seconds_per_eth1_block: u64, + deposit_contract_address: Address, /* TODO: incorporate these into ChainSpec and turn on `serde(deny_unknown_fields)` deposit_chain_id: u64, deposit_network_id: u64, - deposit_contract_address: String, */ } @@ -568,8 +588,8 @@ impl YamlConfig { Self { config_name: T::spec_name().to_string(), // ChainSpec - max_committees_per_slot: spec.max_committees_per_slot, - target_committee_size: spec.target_committee_size, + max_committees_per_slot: spec.max_committees_per_slot as u64, + target_committee_size: spec.target_committee_size as u64, min_per_epoch_churn_limit: spec.min_per_epoch_churn_limit, churn_limit_quotient: spec.churn_limit_quotient, shuffle_round_count: spec.shuffle_round_count, @@ -611,9 +631,9 @@ impl YamlConfig { max_validators_per_committee: T::MaxValidatorsPerCommittee::to_u32(), slots_per_epoch: T::slots_per_epoch(), epochs_per_eth1_voting_period: T::EpochsPerEth1VotingPeriod::to_u64(), - slots_per_historical_root: T::slots_per_historical_root(), - epochs_per_historical_vector: T::epochs_per_historical_vector(), - epochs_per_slashings_vector: T::EpochsPerSlashingsVector::to_usize(), + slots_per_historical_root: T::slots_per_historical_root() as u64, + epochs_per_historical_vector: T::epochs_per_historical_vector() as u64, + epochs_per_slashings_vector: T::EpochsPerSlashingsVector::to_u64(), historical_roots_limit: T::HistoricalRootsLimit::to_u64(), validator_registry_limit: T::ValidatorRegistryLimit::to_u64(), max_proposer_slashings: T::MaxProposerSlashings::to_u32(), @@ -628,6 +648,7 @@ impl YamlConfig { random_subnets_per_validator: spec.random_subnets_per_validator, epochs_per_random_subnet_subscription: spec.epochs_per_random_subnet_subscription, seconds_per_eth1_block: spec.seconds_per_eth1_block, + deposit_contract_address: spec.deposit_contract_address, } } @@ -643,9 +664,9 @@ impl YamlConfig { if self.max_validators_per_committee != T::MaxValidatorsPerCommittee::to_u32() || self.slots_per_epoch != T::slots_per_epoch() || self.epochs_per_eth1_voting_period != T::EpochsPerEth1VotingPeriod::to_u64() - || self.slots_per_historical_root != T::slots_per_historical_root() - || self.epochs_per_historical_vector != T::epochs_per_historical_vector() - || self.epochs_per_slashings_vector != T::EpochsPerSlashingsVector::to_usize() + || self.slots_per_historical_root != T::slots_per_historical_root() as u64 + || self.epochs_per_historical_vector != T::epochs_per_historical_vector() as u64 + || self.epochs_per_slashings_vector != T::EpochsPerSlashingsVector::to_u64() || self.historical_roots_limit != T::HistoricalRootsLimit::to_u64() || self.validator_registry_limit != T::ValidatorRegistryLimit::to_u64() || self.max_proposer_slashings != T::MaxProposerSlashings::to_u32() @@ -662,8 +683,8 @@ impl YamlConfig { /* * Misc */ - max_committees_per_slot: self.max_committees_per_slot, - target_committee_size: self.target_committee_size, + max_committees_per_slot: self.max_committees_per_slot as usize, + target_committee_size: self.target_committee_size as usize, min_per_epoch_churn_limit: self.min_per_epoch_churn_limit, churn_limit_quotient: self.churn_limit_quotient, shuffle_round_count: self.shuffle_round_count, @@ -685,6 +706,7 @@ impl YamlConfig { random_subnets_per_validator: self.random_subnets_per_validator, epochs_per_random_subnet_subscription: self.epochs_per_random_subnet_subscription, seconds_per_eth1_block: self.seconds_per_eth1_block, + deposit_contract_address: self.deposit_contract_address, /* * Gwei values */ diff --git a/consensus/types/src/deposit_data.rs b/consensus/types/src/deposit_data.rs index ce72c362e..8e2050a0b 100644 --- a/consensus/types/src/deposit_data.rs +++ b/consensus/types/src/deposit_data.rs @@ -15,6 +15,7 @@ use tree_hash_derive::TreeHash; pub struct DepositData { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, pub signature: SignatureBytes, } diff --git a/consensus/types/src/deposit_message.rs b/consensus/types/src/deposit_message.rs index fe283a17f..92f6b66bf 100644 --- a/consensus/types/src/deposit_message.rs +++ b/consensus/types/src/deposit_message.rs @@ -15,6 +15,7 @@ use tree_hash_derive::TreeHash; pub struct DepositMessage { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, } diff --git a/consensus/types/src/enr_fork_id.rs b/consensus/types/src/enr_fork_id.rs index e10744368..008b7933f 100644 --- a/consensus/types/src/enr_fork_id.rs +++ b/consensus/types/src/enr_fork_id.rs @@ -1,5 +1,4 @@ use crate::test_utils::TestRandom; -use crate::utils::{fork_from_hex_str, fork_to_hex_str}; use crate::Epoch; use serde_derive::{Deserialize, Serialize}; @@ -16,15 +15,9 @@ use tree_hash_derive::TreeHash; Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] pub struct EnrForkId { - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] pub fork_digest: [u8; 4], - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] pub next_fork_version: [u8; 4], pub next_fork_epoch: Epoch, } diff --git a/consensus/types/src/eth1_data.rs b/consensus/types/src/eth1_data.rs index dcc1ea098..e3b74cc49 100644 --- a/consensus/types/src/eth1_data.rs +++ b/consensus/types/src/eth1_data.rs @@ -26,6 +26,7 @@ use tree_hash_derive::TreeHash; )] pub struct Eth1Data { pub deposit_root: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub deposit_count: u64, pub block_hash: Hash256, } diff --git a/consensus/types/src/fork.rs b/consensus/types/src/fork.rs index 8e95710c4..b129271ba 100644 --- a/consensus/types/src/fork.rs +++ b/consensus/types/src/fork.rs @@ -1,5 +1,4 @@ use crate::test_utils::TestRandom; -use crate::utils::{fork_from_hex_str, fork_to_hex_str}; use crate::Epoch; use serde_derive::{Deserialize, Serialize}; @@ -25,15 +24,9 @@ use tree_hash_derive::TreeHash; TestRandom, )] pub struct Fork { - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] pub previous_version: [u8; 4], - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] pub current_version: [u8; 4], pub epoch: Epoch, } diff --git a/consensus/types/src/fork_data.rs b/consensus/types/src/fork_data.rs index bad6f6219..092102f77 100644 --- a/consensus/types/src/fork_data.rs +++ b/consensus/types/src/fork_data.rs @@ -1,5 +1,4 @@ use crate::test_utils::TestRandom; -use crate::utils::{fork_from_hex_str, fork_to_hex_str}; use crate::{Hash256, SignedRoot}; use serde_derive::{Deserialize, Serialize}; @@ -15,10 +14,7 @@ use tree_hash_derive::TreeHash; Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] pub struct ForkData { - #[serde( - serialize_with = "fork_to_hex_str", - deserialize_with = "fork_from_hex_str" - )] + #[serde(with = "serde_utils::bytes_4_hex")] pub current_version: [u8; 4], pub genesis_validators_root: Hash256, } diff --git a/consensus/types/src/free_attestation.rs b/consensus/types/src/free_attestation.rs index 6215fb0cd..79bc149e4 100644 --- a/consensus/types/src/free_attestation.rs +++ b/consensus/types/src/free_attestation.rs @@ -9,5 +9,6 @@ use serde_derive::Serialize; pub struct FreeAttestation { pub data: AttestationData, pub signature: Signature, + #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, } diff --git a/consensus/types/src/graffiti.rs b/consensus/types/src/graffiti.rs new file mode 100644 index 000000000..f35df9383 --- /dev/null +++ b/consensus/types/src/graffiti.rs @@ -0,0 +1,132 @@ +use crate::{ + test_utils::{RngCore, TestRandom}, + Hash256, +}; +use regex::bytes::Regex; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use ssz::{Decode, DecodeError, Encode}; +use std::fmt; +use tree_hash::TreeHash; + +pub const GRAFFITI_BYTES_LEN: usize = 32; + +/// The 32-byte `graffiti` field on a beacon block. +#[derive(Default, Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[serde(transparent)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +pub struct Graffiti(#[serde(with = "serde_graffiti")] pub [u8; GRAFFITI_BYTES_LEN]); + +impl Graffiti { + pub fn as_utf8_lossy(&self) -> String { + #[allow(clippy::invalid_regex)] + let re = Regex::new("\\p{C}").expect("graffiti regex is valid"); + String::from_utf8_lossy(&re.replace_all(&self.0[..], &b""[..])).to_string() + } +} + +impl fmt::Display for Graffiti { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", serde_utils::hex::encode(&self.0)) + } +} + +impl From<[u8; GRAFFITI_BYTES_LEN]> for Graffiti { + fn from(bytes: [u8; GRAFFITI_BYTES_LEN]) -> Self { + Self(bytes) + } +} + +impl Into<[u8; GRAFFITI_BYTES_LEN]> for Graffiti { + fn into(self) -> [u8; GRAFFITI_BYTES_LEN] { + self.0 + } +} + +pub mod serde_graffiti { + use super::*; + + pub fn serialize<S>(bytes: &[u8; GRAFFITI_BYTES_LEN], serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&serde_utils::hex::encode(bytes)) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; GRAFFITI_BYTES_LEN], D::Error> + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + + let bytes = serde_utils::hex::decode(&s).map_err(D::Error::custom)?; + + if bytes.len() != GRAFFITI_BYTES_LEN { + return Err(D::Error::custom(format!( + "incorrect byte length {}, expected {}", + bytes.len(), + GRAFFITI_BYTES_LEN + ))); + } + + let mut array = [0; GRAFFITI_BYTES_LEN]; + array[..].copy_from_slice(&bytes); + + Ok(array) + } +} + +impl Encode for Graffiti { + fn is_ssz_fixed_len() -> bool { + <[u8; GRAFFITI_BYTES_LEN] as Encode>::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + <[u8; GRAFFITI_BYTES_LEN] as Encode>::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.0.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec<u8>) { + self.0.ssz_append(buf) + } +} + +impl Decode for Graffiti { + fn is_ssz_fixed_len() -> bool { + <[u8; GRAFFITI_BYTES_LEN] as Decode>::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + <[u8; GRAFFITI_BYTES_LEN] as Decode>::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> { + <[u8; GRAFFITI_BYTES_LEN]>::from_ssz_bytes(bytes).map(Self) + } +} + +impl TreeHash for Graffiti { + fn tree_hash_type() -> tree_hash::TreeHashType { + <[u8; GRAFFITI_BYTES_LEN]>::tree_hash_type() + } + + fn tree_hash_packed_encoding(&self) -> Vec<u8> { + self.0.tree_hash_packed_encoding() + } + + fn tree_hash_packing_factor() -> usize { + <[u8; GRAFFITI_BYTES_LEN]>::tree_hash_packing_factor() + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + self.0.tree_hash_root() + } +} + +impl TestRandom for Graffiti { + fn random_for_test(rng: &mut impl RngCore) -> Self { + Self::from(Hash256::random_for_test(rng).to_fixed_bytes()) + } +} diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/indexed_attestation.rs index 341db1807..eaae75de8 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/indexed_attestation.rs @@ -18,6 +18,7 @@ use tree_hash_derive::TreeHash; #[serde(bound = "T: EthSpec")] pub struct IndexedAttestation<T: EthSpec> { /// Lists validator registry indices, not committee indices. + #[serde(with = "quoted_variable_list_u64")] pub attesting_indices: VariableList<u64, T::MaxValidatorsPerCommittee>, pub data: AttestationData, pub signature: AggregateSignature, @@ -53,6 +54,43 @@ impl<T: EthSpec> Hash for IndexedAttestation<T> { } } +/// Serialize a variable list of `u64` such that each int is quoted. Deserialize a variable +/// list supporting both quoted and un-quoted ints. +/// +/// E.g.,`["0", "1", "2"]` +mod quoted_variable_list_u64 { + use super::*; + use crate::Unsigned; + use serde::ser::SerializeSeq; + use serde::{Deserializer, Serializer}; + use serde_utils::quoted_u64_vec::{QuotedIntVecVisitor, QuotedIntWrapper}; + + pub fn serialize<S, T>(value: &VariableList<u64, T>, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + T: Unsigned, + { + let mut seq = serializer.serialize_seq(Some(value.len()))?; + for &int in value.iter() { + seq.serialize_element(&QuotedIntWrapper { int })?; + } + seq.end() + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result<VariableList<u64, T>, D::Error> + where + D: Deserializer<'de>, + T: Unsigned, + { + deserializer + .deserialize_any(QuotedIntVecVisitor) + .and_then(|vec| { + VariableList::new(vec) + .map_err(|e| serde::de::Error::custom(format!("invalid length: {:?}", e))) + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 19697118a..65c1290d7 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -29,19 +29,21 @@ pub mod eth_spec; pub mod fork; pub mod fork_data; pub mod free_attestation; +pub mod graffiti; pub mod historical_batch; pub mod indexed_attestation; pub mod pending_attestation; pub mod proposer_slashing; pub mod relative_epoch; pub mod selection_proof; +pub mod shuffling_id; pub mod signed_aggregate_and_proof; pub mod signed_beacon_block; pub mod signed_beacon_block_header; pub mod signed_voluntary_exit; pub mod signing_data; -pub mod utils; pub mod validator; +pub mod validator_subscription; pub mod voluntary_exit; #[macro_use] pub mod slot_epoch_macros; @@ -74,12 +76,14 @@ pub use crate::eth1_data::Eth1Data; pub use crate::fork::Fork; pub use crate::fork_data::ForkData; pub use crate::free_attestation::FreeAttestation; +pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::indexed_attestation::IndexedAttestation; pub use crate::pending_attestation::PendingAttestation; pub use crate::proposer_slashing::ProposerSlashing; pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; pub use crate::selection_proof::SelectionProof; +pub use crate::shuffling_id::ShufflingId; pub use crate::signed_aggregate_and_proof::SignedAggregateAndProof; pub use crate::signed_beacon_block::{SignedBeaconBlock, SignedBeaconBlockHash}; pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader; @@ -88,6 +92,7 @@ pub use crate::signing_data::{SignedRoot, SigningData}; pub use crate::slot_epoch::{Epoch, Slot}; pub use crate::subnet_id::SubnetId; pub use crate::validator::Validator; +pub use crate::validator_subscription::ValidatorSubscription; pub use crate::voluntary_exit::VoluntaryExit; pub type CommitteeIndex = u64; @@ -99,4 +104,3 @@ pub use bls::{ AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes, }; pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector, VariableList}; -pub use utils::{Graffiti, GRAFFITI_BYTES_LEN}; diff --git a/consensus/types/src/pending_attestation.rs b/consensus/types/src/pending_attestation.rs index 70ebb1bbd..f4b0fd9b1 100644 --- a/consensus/types/src/pending_attestation.rs +++ b/consensus/types/src/pending_attestation.rs @@ -13,7 +13,9 @@ use tree_hash_derive::TreeHash; pub struct PendingAttestation<T: EthSpec> { pub aggregation_bits: BitList<T::MaxValidatorsPerCommittee>, pub data: AttestationData, + #[serde(with = "serde_utils::quoted_u64")] pub inclusion_delay: u64, + #[serde(with = "serde_utils::quoted_u64")] pub proposer_index: u64, } diff --git a/consensus/types/src/shuffling_id.rs b/consensus/types/src/shuffling_id.rs new file mode 100644 index 000000000..d54b5fa64 --- /dev/null +++ b/consensus/types/src/shuffling_id.rs @@ -0,0 +1,61 @@ +use crate::*; +use serde_derive::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use std::hash::Hash; + +/// Can be used to key (ID) the shuffling in some chain, in some epoch. +/// +/// ## Reasoning +/// +/// We say that the ID of some shuffling is always equal to a 2-tuple: +/// +/// - The epoch for which the shuffling should be effective. +/// - A block root, where this is the root at the *last* slot of the penultimate epoch. I.e., the +/// final block which contributed a randao reveal to the seed for the shuffling. +/// +/// The struct stores exactly that 2-tuple. +#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, Encode, Decode)] +pub struct ShufflingId { + pub shuffling_epoch: Epoch, + shuffling_decision_block: Hash256, +} + +impl ShufflingId { + /// Using the given `state`, return the shuffling id for the shuffling at the given + /// `relative_epoch`. + /// + /// The `block_root` provided should be either: + /// + /// - The root of the block which produced this state. + /// - If the state is from a skip slot, the root of the latest block in that state. + pub fn new<E: EthSpec>( + block_root: Hash256, + state: &BeaconState<E>, + relative_epoch: RelativeEpoch, + ) -> Result<Self, BeaconStateError> { + let shuffling_epoch = relative_epoch.into_epoch(state.current_epoch()); + + let shuffling_decision_slot = shuffling_epoch + .saturating_sub(1_u64) + .start_slot(E::slots_per_epoch()) + .saturating_sub(1_u64); + + let shuffling_decision_block = if state.slot == shuffling_decision_slot { + block_root + } else { + *state.get_block_root(shuffling_decision_slot)? + }; + + Ok(Self { + shuffling_epoch, + shuffling_decision_block, + }) + } + + pub fn from_components(shuffling_epoch: Epoch, shuffling_decision_block: Hash256) -> Self { + Self { + shuffling_epoch, + shuffling_decision_block, + } + } +} diff --git a/consensus/types/src/slot_epoch_macros.rs b/consensus/types/src/slot_epoch_macros.rs index 26b80692c..caf31417d 100644 --- a/consensus/types/src/slot_epoch_macros.rs +++ b/consensus/types/src/slot_epoch_macros.rs @@ -313,6 +313,18 @@ macro_rules! impl_ssz { }; } +macro_rules! impl_from_str { + ($type: ident) => { + impl std::str::FromStr for $type { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result<$type, Self::Err> { + u64::from_str(s).map($type) + } + } + }; +} + macro_rules! impl_common { ($type: ident) => { impl_from_into_u64!($type); @@ -328,6 +340,7 @@ macro_rules! impl_common { impl_display!($type); impl_debug!($type); impl_ssz!($type); + impl_from_str!($type); }; } diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index 80cc24977..667e2c9b7 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -6,7 +6,8 @@ use std::ops::{Deref, DerefMut}; #[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct SubnetId(u64); +#[serde(transparent)] +pub struct SubnetId(#[serde(with = "serde_utils::quoted_u64")] u64); impl SubnetId { pub fn new(id: u64) -> Self { diff --git a/consensus/types/src/utils.rs b/consensus/types/src/utils.rs deleted file mode 100644 index a527fc18f..000000000 --- a/consensus/types/src/utils.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod serde_utils; - -pub use self::serde_utils::*; diff --git a/consensus/types/src/utils/serde_utils.rs b/consensus/types/src/utils/serde_utils.rs deleted file mode 100644 index 36b719646..000000000 --- a/consensus/types/src/utils/serde_utils.rs +++ /dev/null @@ -1,134 +0,0 @@ -use serde::de::Error; -use serde::{Deserialize, Deserializer, Serializer}; - -pub const FORK_BYTES_LEN: usize = 4; -pub const GRAFFITI_BYTES_LEN: usize = 32; - -/// Type for a slice of `GRAFFITI_BYTES_LEN` bytes. -/// -/// Gets included inside each `BeaconBlockBody`. -pub type Graffiti = [u8; GRAFFITI_BYTES_LEN]; - -pub fn u8_from_hex_str<'de, D>(deserializer: D) -> Result<u8, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - - let start = match s.as_str().get(2..) { - Some(start) => start, - None => return Err(D::Error::custom("string length too small")), - }; - u8::from_str_radix(&start, 16).map_err(D::Error::custom) -} - -#[allow(clippy::trivially_copy_pass_by_ref)] // Serde requires the `byte` to be a ref. -pub fn u8_to_hex_str<S>(byte: &u8, serializer: S) -> Result<S::Ok, S::Error> -where - S: Serializer, -{ - let mut hex: String = "0x".to_string(); - hex.push_str(&hex::encode(&[*byte])); - - serializer.serialize_str(&hex) -} - -pub fn u32_from_hex_str<'de, D>(deserializer: D) -> Result<u32, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let start = s - .as_str() - .get(2..) - .ok_or_else(|| D::Error::custom("string length too small"))?; - - u32::from_str_radix(&start, 16) - .map_err(D::Error::custom) - .map(u32::from_be) -} - -#[allow(clippy::trivially_copy_pass_by_ref)] // Serde requires the `num` to be a ref. -pub fn u32_to_hex_str<S>(num: &u32, serializer: S) -> Result<S::Ok, S::Error> -where - S: Serializer, -{ - let mut hex: String = "0x".to_string(); - let bytes = num.to_le_bytes(); - hex.push_str(&hex::encode(&bytes)); - - serializer.serialize_str(&hex) -} - -pub fn fork_from_hex_str<'de, D>(deserializer: D) -> Result<[u8; FORK_BYTES_LEN], D::Error> -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let mut array = [0 as u8; FORK_BYTES_LEN]; - - let start = s - .as_str() - .get(2..) - .ok_or_else(|| D::Error::custom("string length too small"))?; - let decoded: Vec<u8> = hex::decode(&start).map_err(D::Error::custom)?; - - if decoded.len() != FORK_BYTES_LEN { - return Err(D::Error::custom("Fork length too long")); - } - - for (i, item) in array.iter_mut().enumerate() { - if i > decoded.len() { - break; - } - *item = decoded[i]; - } - Ok(array) -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -pub fn fork_to_hex_str<S>(bytes: &[u8; FORK_BYTES_LEN], serializer: S) -> Result<S::Ok, S::Error> -where - S: Serializer, -{ - let mut hex_string: String = "0x".to_string(); - hex_string.push_str(&hex::encode(&bytes)); - - serializer.serialize_str(&hex_string) -} - -pub fn graffiti_to_hex_str<S>(bytes: &Graffiti, serializer: S) -> Result<S::Ok, S::Error> -where - S: Serializer, -{ - let mut hex_string: String = "0x".to_string(); - hex_string.push_str(&hex::encode(&bytes)); - - serializer.serialize_str(&hex_string) -} - -pub fn graffiti_from_hex_str<'de, D>(deserializer: D) -> Result<Graffiti, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let mut array = Graffiti::default(); - - let start = s - .as_str() - .get(2..) - .ok_or_else(|| D::Error::custom("string length too small"))?; - let decoded: Vec<u8> = hex::decode(&start).map_err(D::Error::custom)?; - - if decoded.len() > GRAFFITI_BYTES_LEN { - return Err(D::Error::custom("Fork length too long")); - } - - for (i, item) in array.iter_mut().enumerate() { - if i > decoded.len() { - break; - } - *item = decoded[i]; - } - Ok(array) -} diff --git a/consensus/types/src/validator_subscription.rs b/consensus/types/src/validator_subscription.rs new file mode 100644 index 000000000..fd48660c5 --- /dev/null +++ b/consensus/types/src/validator_subscription.rs @@ -0,0 +1,21 @@ +use crate::*; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; + +/// A validator subscription, created when a validator subscribes to a slot to perform optional aggregation +/// duties. +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] +pub struct ValidatorSubscription { + /// The validators index. + pub validator_index: u64, + /// The index of the committee within `slot` of which the validator is a member. Used by the + /// beacon node to quickly evaluate the associated `SubnetId`. + pub attestation_committee_index: CommitteeIndex, + /// The slot in which to subscribe. + pub slot: Slot, + /// Committee count at slot to subscribe. + pub committee_count_at_slot: u64, + /// If true, the validator is an aggregator and the beacon node should aggregate attestations + /// for this slot. + pub is_aggregator: bool, +} diff --git a/consensus/types/src/voluntary_exit.rs b/consensus/types/src/voluntary_exit.rs index a9509d7af..c33ea7e79 100644 --- a/consensus/types/src/voluntary_exit.rs +++ b/consensus/types/src/voluntary_exit.rs @@ -16,6 +16,7 @@ use tree_hash_derive::TreeHash; pub struct VoluntaryExit { /// Earliest epoch when voluntary exit can be processed. pub epoch: Epoch, + #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, } diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index e1cb1fde3..8fd004a80 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -11,7 +11,7 @@ milagro_bls = { git = "https://github.com/sigp/milagro_bls", branch = "paulh" } rand = "0.7.2" serde = "1.0.102" serde_derive = "1.0.102" -serde_hex = { path = "../../consensus/serde_hex" } +serde_utils = { path = "../../consensus/serde_utils" } hex = "0.3" eth2_hashing = "0.1.0" ethereum-types = "0.9.1" diff --git a/crypto/bls/src/generic_aggregate_signature.rs b/crypto/bls/src/generic_aggregate_signature.rs index 240b7d188..0517512f8 100644 --- a/crypto/bls/src/generic_aggregate_signature.rs +++ b/crypto/bls/src/generic_aggregate_signature.rs @@ -6,7 +6,7 @@ use crate::{ }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; +use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; use std::fmt; use std::marker::PhantomData; @@ -245,6 +245,23 @@ where impl_tree_hash!(SIGNATURE_BYTES_LEN); } +impl<Pub, AggPub, Sig, AggSig> fmt::Display for GenericAggregateSignature<Pub, AggPub, Sig, AggSig> +where + Sig: TSignature<Pub>, + AggSig: TAggregateSignature<Pub, AggPub, Sig>, +{ + impl_display!(); +} + +impl<Pub, AggPub, Sig, AggSig> std::str::FromStr + for GenericAggregateSignature<Pub, AggPub, Sig, AggSig> +where + Sig: TSignature<Pub>, + AggSig: TAggregateSignature<Pub, AggPub, Sig>, +{ + impl_from_str!(); +} + impl<Pub, AggPub, Sig, AggSig> Serialize for GenericAggregateSignature<Pub, AggPub, Sig, AggSig> where Sig: TSignature<Pub>, diff --git a/crypto/bls/src/generic_public_key.rs b/crypto/bls/src/generic_public_key.rs index 29814d24a..7b22d2729 100644 --- a/crypto/bls/src/generic_public_key.rs +++ b/crypto/bls/src/generic_public_key.rs @@ -1,7 +1,7 @@ use crate::Error; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; +use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; use std::fmt; use std::hash::{Hash, Hasher}; @@ -97,6 +97,14 @@ impl<Pub: TPublicKey> TreeHash for GenericPublicKey<Pub> { impl_tree_hash!(PUBLIC_KEY_BYTES_LEN); } +impl<Pub: TPublicKey> fmt::Display for GenericPublicKey<Pub> { + impl_display!(); +} + +impl<Pub: TPublicKey> std::str::FromStr for GenericPublicKey<Pub> { + impl_from_str!(); +} + impl<Pub: TPublicKey> Serialize for GenericPublicKey<Pub> { impl_serde_serialize!(); } diff --git a/crypto/bls/src/generic_public_key_bytes.rs b/crypto/bls/src/generic_public_key_bytes.rs index beceac1c9..387eb91c9 100644 --- a/crypto/bls/src/generic_public_key_bytes.rs +++ b/crypto/bls/src/generic_public_key_bytes.rs @@ -4,7 +4,7 @@ use crate::{ }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; +use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; use std::convert::TryInto; use std::fmt; @@ -101,6 +101,16 @@ where Pub: TPublicKey, { fn from(pk: GenericPublicKey<Pub>) -> Self { + Self::from(&pk) + } +} + +/// Serializes the `PublicKey` in compressed form, storing the bytes in the newly created `Self`. +impl<Pub> From<&GenericPublicKey<Pub>> for GenericPublicKeyBytes<Pub> +where + Pub: TPublicKey, +{ + fn from(pk: &GenericPublicKey<Pub>) -> Self { Self { bytes: pk.serialize(), _phantom: PhantomData, @@ -132,6 +142,14 @@ impl<Pub> TreeHash for GenericPublicKeyBytes<Pub> { impl_tree_hash!(PUBLIC_KEY_BYTES_LEN); } +impl<Pub> fmt::Display for GenericPublicKeyBytes<Pub> { + impl_display!(); +} + +impl<Pub> std::str::FromStr for GenericPublicKeyBytes<Pub> { + impl_from_str!(); +} + impl<Pub> Serialize for GenericPublicKeyBytes<Pub> { impl_serde_serialize!(); } diff --git a/crypto/bls/src/generic_signature.rs b/crypto/bls/src/generic_signature.rs index 28a936195..44250d4a6 100644 --- a/crypto/bls/src/generic_signature.rs +++ b/crypto/bls/src/generic_signature.rs @@ -4,7 +4,7 @@ use crate::{ }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; +use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; use std::fmt; use std::marker::PhantomData; @@ -149,6 +149,14 @@ impl<PublicKey, T: TSignature<PublicKey>> TreeHash for GenericSignature<PublicKe impl_tree_hash!(SIGNATURE_BYTES_LEN); } +impl<PublicKey, T: TSignature<PublicKey>> fmt::Display for GenericSignature<PublicKey, T> { + impl_display!(); +} + +impl<PublicKey, T: TSignature<PublicKey>> std::str::FromStr for GenericSignature<PublicKey, T> { + impl_from_str!(); +} + impl<PublicKey, T: TSignature<PublicKey>> Serialize for GenericSignature<PublicKey, T> { impl_serde_serialize!(); } diff --git a/crypto/bls/src/generic_signature_bytes.rs b/crypto/bls/src/generic_signature_bytes.rs index 1f987ecd3..bc7e7f111 100644 --- a/crypto/bls/src/generic_signature_bytes.rs +++ b/crypto/bls/src/generic_signature_bytes.rs @@ -5,7 +5,7 @@ use crate::{ }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; +use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; use std::convert::TryInto; use std::fmt; @@ -124,6 +124,14 @@ impl<Pub, Sig> TreeHash for GenericSignatureBytes<Pub, Sig> { impl_tree_hash!(SIGNATURE_BYTES_LEN); } +impl<Pub, Sig> fmt::Display for GenericSignatureBytes<Pub, Sig> { + impl_display!(); +} + +impl<Pub, Sig> std::str::FromStr for GenericSignatureBytes<Pub, Sig> { + impl_from_str!(); +} + impl<Pub, Sig> Serialize for GenericSignatureBytes<Pub, Sig> { impl_serde_serialize!(); } diff --git a/crypto/bls/src/macros.rs b/crypto/bls/src/macros.rs index ca103da6d..136faeb44 100644 --- a/crypto/bls/src/macros.rs +++ b/crypto/bls/src/macros.rs @@ -76,6 +76,35 @@ macro_rules! impl_ssz_decode { }; } +/// Contains the functions required for a `fmt::Display` implementation. +/// +/// Does not include the `Impl` section since it gets very complicated when it comes to generics. +macro_rules! impl_display { + () => { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex_encode(self.serialize().to_vec())) + } + }; +} + +/// Contains the functions required for a `fmt::Display` implementation. +/// +/// Does not include the `Impl` section since it gets very complicated when it comes to generics. +macro_rules! impl_from_str { + () => { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.starts_with("0x") { + let bytes = hex::decode(&s[2..]).map_err(|e| e.to_string())?; + Self::deserialize(&bytes[..]).map_err(|e| format!("{:?}", e)) + } else { + Err("must start with 0x".to_string()) + } + } + }; +} + /// Contains the functions required for a `serde::Serialize` implementation. /// /// Does not include the `Impl` section since it gets very complicated when it comes to generics. @@ -85,7 +114,7 @@ macro_rules! impl_serde_serialize { where S: Serializer, { - serializer.serialize_str(&hex_encode(self.serialize().to_vec())) + serializer.serialize_str(&self.to_string()) } }; } @@ -99,9 +128,25 @@ macro_rules! impl_serde_deserialize { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; - Self::deserialize(&bytes[..]) - .map_err(|e| serde::de::Error::custom(format!("invalid pubkey ({:?})", e))) + pub struct StringVisitor; + + impl<'de> serde::de::Visitor<'de> for StringVisitor { + type Value = String; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a hex string with 0x prefix") + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(value.to_string()) + } + } + + let string = deserializer.deserialize_str(StringVisitor)?; + <Self as std::str::FromStr>::from_str(&string).map_err(serde::de::Error::custom) } }; } diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index a48f24f3f..ae2393636 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -15,6 +15,6 @@ url = "2.1.1" serde = "1.0.110" futures = "0.3.5" genesis = { path = "../../beacon_node/genesis" } -remote_beacon_node = { path = "../../common/remote_beacon_node" } +eth2 = { path = "../../common/eth2" } validator_client = { path = "../../validator_client" } validator_dir = { path = "../../common/validator_dir", features = ["insecure_keys"] } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index b1a74b64a..e2391c0f8 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -4,7 +4,12 @@ use beacon_node::ProductionBeaconNode; use environment::RuntimeContext; +use eth2::{ + reqwest::{ClientBuilder, Url}, + BeaconNodeHttpClient, +}; use std::path::PathBuf; +use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use tempdir::TempDir; use types::EthSpec; @@ -13,9 +18,12 @@ use validator_dir::insecure_keys::build_deterministic_validator_dirs; pub use beacon_node::{ClientConfig, ClientGenesis, ProductionClient}; pub use environment; -pub use remote_beacon_node::RemoteBeaconNode; +pub use eth2; pub use validator_client::Config as ValidatorConfig; +/// The global timeout for HTTP requests to the beacon node. +const HTTP_TIMEOUT: Duration = Duration::from_secs(4); + /// Provides a beacon node that is running in the current process on a given tokio executor (it /// is _local_ to this process). /// @@ -52,16 +60,23 @@ impl<E: EthSpec> LocalBeaconNode<E> { impl<E: EthSpec> LocalBeaconNode<E> { /// Returns a `RemoteBeaconNode` that can connect to `self`. Useful for testing the node as if /// it were external this process. - pub fn remote_node(&self) -> Result<RemoteBeaconNode<E>, String> { - let socket_addr = self + pub fn remote_node(&self) -> Result<BeaconNodeHttpClient, String> { + let listen_addr = self .client - .http_listen_addr() + .http_api_listen_addr() .ok_or_else(|| "A remote beacon node must have a http server".to_string())?; - Ok(RemoteBeaconNode::new(format!( - "http://{}:{}", - socket_addr.ip(), - socket_addr.port() - ))?) + + let beacon_node_url: Url = format!("http://{}:{}", listen_addr.ip(), listen_addr.port()) + .parse() + .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; + let beacon_node_http_client = ClientBuilder::new() + .timeout(HTTP_TIMEOUT) + .build() + .map_err(|e| format!("Unable to build HTTP client: {:?}", e))?; + Ok(BeaconNodeHttpClient::from_components( + beacon_node_url, + beacon_node_http_client, + )) } } @@ -71,8 +86,8 @@ pub fn testing_client_config() -> ClientConfig { // Setting ports to `0` means that the OS will choose some available port. client_config.network.libp2p_port = 0; client_config.network.discovery_port = 0; - client_config.rest_api.enabled = true; - client_config.rest_api.port = 0; + client_config.http_api.enabled = true; + client_config.http_api.listen_port = 0; client_config.websocket_server.enabled = true; client_config.websocket_server.port = 0; diff --git a/testing/simulator/src/checks.rs b/testing/simulator/src/checks.rs index 43ceaa14f..e755c9005 100644 --- a/testing/simulator/src/checks.rs +++ b/testing/simulator/src/checks.rs @@ -1,4 +1,5 @@ use crate::local_network::LocalNetwork; +use node_test_rig::eth2::types::StateId; use std::time::Duration; use types::{Epoch, EthSpec, Slot, Unsigned}; @@ -65,11 +66,9 @@ pub async fn verify_all_finalized_at<E: EthSpec>( for remote_node in network.remote_nodes()? { epochs.push( remote_node - .http - .beacon() - .get_head() + .get_beacon_states_finality_checkpoints(StateId::Head) .await - .map(|head| head.finalized_slot.epoch(E::slots_per_epoch())) + .map(|body| body.unwrap().data.finalized.epoch) .map_err(|e| format!("Get head via http failed: {:?}", e))?, ); } @@ -95,17 +94,10 @@ async fn verify_validator_count<E: EthSpec>( let validator_counts = { let mut validator_counts = Vec::new(); for remote_node in network.remote_nodes()? { - let beacon = remote_node.http.beacon(); - - let head = beacon - .get_head() + let vc = remote_node + .get_debug_beacon_states::<E>(StateId::Head) .await - .map_err(|e| format!("Get head via http failed: {:?}", e))?; - - let vc = beacon - .get_state_by_root(head.state_root) - .await - .map(|(state, _root)| state) + .map(|body| body.unwrap().data) .map_err(|e| format!("Get state root via http failed: {:?}", e))? .validators .len(); diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index de78aaa05..1ce8b2a5d 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -34,7 +34,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .short("s") .long("speed_up_factor") .takes_value(true) - .default_value("4") + .default_value("3") .help("Speed up factor")) .arg(Arg::with_name("continue_after_checks") .short("c") @@ -62,7 +62,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .short("s") .long("speed_up_factor") .takes_value(true) - .default_value("4") + .default_value("3") .help("Speed up factor")) .arg(Arg::with_name("continue_after_checks") .short("c") diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 37ce3ab56..0dd9b3424 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -1,6 +1,7 @@ use node_test_rig::{ - environment::RuntimeContext, ClientConfig, LocalBeaconNode, LocalValidatorClient, - RemoteBeaconNode, ValidatorConfig, ValidatorFiles, + environment::RuntimeContext, + eth2::{types::StateId, BeaconNodeHttpClient}, + ClientConfig, LocalBeaconNode, LocalValidatorClient, ValidatorConfig, ValidatorFiles, }; use parking_lot::RwLock; use std::ops::Deref; @@ -123,7 +124,7 @@ impl<E: EthSpec> LocalNetwork<E> { .ok_or_else(|| format!("No beacon node for index {}", beacon_node))?; beacon_node .client - .http_listen_addr() + .http_api_listen_addr() .expect("Must have http started") }; @@ -140,7 +141,7 @@ impl<E: EthSpec> LocalNetwork<E> { } /// For all beacon nodes in `Self`, return a HTTP client to access each nodes HTTP API. - pub fn remote_nodes(&self) -> Result<Vec<RemoteBeaconNode<E>>, String> { + pub fn remote_nodes(&self) -> Result<Vec<BeaconNodeHttpClient>, String> { let beacon_nodes = self.beacon_nodes.read(); beacon_nodes @@ -154,11 +155,9 @@ impl<E: EthSpec> LocalNetwork<E> { let nodes = self.remote_nodes().expect("Failed to get remote nodes"); let bootnode = nodes.first().expect("Should contain bootnode"); bootnode - .http - .beacon() - .get_head() + .get_beacon_states_finality_checkpoints(StateId::Head) .await .map_err(|e| format!("Cannot get head: {:?}", e)) - .map(|head| head.finalized_slot.epoch(E::slots_per_epoch())) + .map(|body| body.unwrap().data.finalized.epoch) } } diff --git a/testing/simulator/src/sync_sim.rs b/testing/simulator/src/sync_sim.rs index 7583a6eab..47272f626 100644 --- a/testing/simulator/src/sync_sim.rs +++ b/testing/simulator/src/sync_sim.rs @@ -350,11 +350,9 @@ pub async fn check_still_syncing<E: EthSpec>(network: &LocalNetwork<E>) -> Resul for remote_node in network.remote_nodes()? { status.push( remote_node - .http - .node() - .syncing_status() + .get_node_syncing() .await - .map(|status| status.is_syncing) + .map(|body| body.data.is_syncing) .map_err(|e| format!("Get syncing status via http failed: {:?}", e))?, ) } diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 77a6e5ce9..b69b31f57 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -19,7 +19,6 @@ clap = "2.33.0" eth2_interop_keypairs = { path = "../common/eth2_interop_keypairs" } slashing_protection = { path = "./slashing_protection" } slot_clock = { path = "../common/slot_clock" } -rest_types = { path = "../common/rest_types" } types = { path = "../consensus/types" } serde = "1.0.110" serde_derive = "1.0.110" @@ -41,7 +40,7 @@ eth2_ssz_derive = "0.1.0" hex = "0.4.2" deposit_contract = { path = "../common/deposit_contract" } bls = { path = "../crypto/bls" } -remote_beacon_node = { path = "../common/remote_beacon_node" } +eth2 = { path = "../common/eth2" } tempdir = "0.3.7" rayon = "1.3.0" validator_dir = { path = "../common/validator_dir" } diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index fa7987777..d675ebda2 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -3,22 +3,26 @@ use crate::{ validator_store::ValidatorStore, }; use environment::RuntimeContext; +use eth2::BeaconNodeHttpClient; use futures::StreamExt; -use remote_beacon_node::{PublishStatus, RemoteBeaconNode}; -use slog::{crit, debug, error, info, trace}; +use slog::{crit, error, info, trace}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; use tokio::time::{delay_until, interval_at, Duration, Instant}; -use types::{Attestation, ChainSpec, CommitteeIndex, EthSpec, Slot, SubnetId}; +use tree_hash::TreeHash; +use types::{ + AggregateSignature, Attestation, AttestationData, BitList, ChainSpec, CommitteeIndex, EthSpec, + Slot, +}; /// Builds an `AttestationService`. pub struct AttestationServiceBuilder<T, E: EthSpec> { duties_service: Option<DutiesService<T, E>>, validator_store: Option<ValidatorStore<T, E>>, slot_clock: Option<T>, - beacon_node: Option<RemoteBeaconNode<E>>, + beacon_node: Option<BeaconNodeHttpClient>, context: Option<RuntimeContext<E>>, } @@ -48,7 +52,7 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationServiceBuilder<T, E> { self } - pub fn beacon_node(mut self, beacon_node: RemoteBeaconNode<E>) -> Self { + pub fn beacon_node(mut self, beacon_node: BeaconNodeHttpClient) -> Self { self.beacon_node = Some(beacon_node); self } @@ -86,7 +90,7 @@ pub struct Inner<T, E: EthSpec> { duties_service: DutiesService<T, E>, validator_store: ValidatorStore<T, E>, slot_clock: T, - beacon_node: RemoteBeaconNode<E>, + beacon_node: BeaconNodeHttpClient, context: RuntimeContext<E>, } @@ -262,7 +266,7 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { // Step 2. // // If an attestation was produced, make an aggregate. - if let Some(attestation) = attestation_opt { + if let Some(attestation_data) = attestation_opt { // First, wait until the `aggregation_production_instant` (2/3rds // of the way though the slot). As verified in the // `delay_triggers_when_in_the_past` test, this code will still run @@ -272,7 +276,7 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { // Then download, sign and publish a `SignedAggregateAndProof` for each // validator that is elected to aggregate for this `slot` and // `committee_index`. - self.produce_and_publish_aggregates(attestation, &validator_duties) + self.produce_and_publish_aggregates(attestation_data, &validator_duties) .await .map_err(move |e| { crit!( @@ -305,7 +309,7 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { slot: Slot, committee_index: CommitteeIndex, validator_duties: &[DutyAndProof], - ) -> Result<Option<Attestation<E>>, String> { + ) -> Result<Option<AttestationData>, String> { let log = self.context.log(); if validator_duties.is_empty() { @@ -318,124 +322,88 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { .ok_or_else(|| "Unable to determine current slot from clock".to_string())? .epoch(E::slots_per_epoch()); - let attestation = self + let attestation_data = self .beacon_node - .http - .validator() - .produce_attestation(slot, committee_index) + .get_validator_attestation_data(slot, committee_index) .await - .map_err(|e| format!("Failed to produce attestation: {:?}", e))?; + .map_err(|e| format!("Failed to produce attestation data: {:?}", e))? + .data; - // For each validator in `validator_duties`, clone the `attestation` and add - // their signature. - // - // If any validator is unable to sign, they are simply skipped. - let signed_attestations = validator_duties - .iter() - .filter_map(|duty| { - // Ensure that all required fields are present in the validator duty. - let ( - duty_slot, - duty_committee_index, + for duty in validator_duties { + // Ensure that all required fields are present in the validator duty. + let ( + duty_slot, + duty_committee_index, + validator_committee_position, + _, + _, + committee_length, + ) = if let Some(tuple) = duty.attestation_duties() { + tuple + } else { + crit!( + log, + "Missing validator duties when signing"; + "duties" => format!("{:?}", duty) + ); + continue; + }; + + // Ensure that the attestation matches the duties. + if duty_slot != attestation_data.slot || duty_committee_index != attestation_data.index + { + crit!( + log, + "Inconsistent validator duties during signing"; + "validator" => format!("{:?}", duty.validator_pubkey()), + "duty_slot" => duty_slot, + "attestation_slot" => attestation_data.slot, + "duty_index" => duty_committee_index, + "attestation_index" => attestation_data.index, + ); + continue; + } + + let mut attestation = Attestation { + aggregation_bits: BitList::with_capacity(committee_length as usize).unwrap(), + data: attestation_data.clone(), + signature: AggregateSignature::infinity(), + }; + + self.validator_store + .sign_attestation( + duty.validator_pubkey(), validator_committee_position, - _, - committee_count_at_slot, - ) = if let Some(tuple) = duty.attestation_duties() { - tuple - } else { - crit!( - log, - "Missing validator duties when signing"; - "duties" => format!("{:?}", duty) - ); - return None; - }; - - // Ensure that the attestation matches the duties. - if duty_slot != attestation.data.slot - || duty_committee_index != attestation.data.index - { - crit!( - log, - "Inconsistent validator duties during signing"; - "validator" => format!("{:?}", duty.validator_pubkey()), - "duty_slot" => duty_slot, - "attestation_slot" => attestation.data.slot, - "duty_index" => duty_committee_index, - "attestation_index" => attestation.data.index, - ); - return None; - } - - let mut attestation = attestation.clone(); - let subnet_id = SubnetId::compute_subnet_for_attestation_data::<E>( - &attestation.data, - committee_count_at_slot, - &self.context.eth2_config().spec, + &mut attestation, + current_epoch, ) - .map_err(|e| { - error!( - log, - "Failed to compute subnet id to publish attestation: {:?}", e - ) - }) - .ok()?; - self.validator_store - .sign_attestation( - duty.validator_pubkey(), - validator_committee_position, - &mut attestation, - current_epoch, - ) - .map(|_| (attestation, subnet_id)) - }) - .collect::<Vec<_>>(); + .ok_or_else(|| "Failed to sign attestation".to_string())?; - // If there are any signed attestations, publish them to the BN. Otherwise, - // just return early. - if let Some(attestation) = signed_attestations.first().cloned() { - let num_attestations = signed_attestations.len(); - let beacon_block_root = attestation.0.data.beacon_block_root; - - self.beacon_node - .http - .validator() - .publish_attestations(signed_attestations) + match self + .beacon_node + .post_beacon_pool_attestations(&attestation) .await - .map_err(|e| format!("Failed to publish attestation: {:?}", e)) - .map(move |publish_status| match publish_status { - PublishStatus::Valid => info!( - log, - "Successfully published attestations"; - "count" => num_attestations, - "head_block" => format!("{:?}", beacon_block_root), - "committee_index" => committee_index, - "slot" => slot.as_u64(), - "type" => "unaggregated", - ), - PublishStatus::Invalid(msg) => crit!( - log, - "Published attestation was invalid"; - "message" => msg, - "committee_index" => committee_index, - "slot" => slot.as_u64(), - "type" => "unaggregated", - ), - PublishStatus::Unknown => { - crit!(log, "Unknown condition when publishing unagg. attestation") - } - }) - .map(|()| Some(attestation.0)) - } else { - debug!( - log, - "No attestations to publish"; - "committee_index" => committee_index, - "slot" => slot.as_u64(), - ); - - Ok(None) + { + Ok(()) => info!( + log, + "Successfully published attestation"; + "head_block" => format!("{:?}", attestation.data.beacon_block_root), + "committee_index" => attestation.data.index, + "slot" => attestation.data.slot.as_u64(), + "type" => "unaggregated", + ), + Err(e) => error!( + log, + "Unable to publish attestation"; + "error" => e.to_string(), + "committee_index" => attestation.data.index, + "slot" => slot.as_u64(), + "type" => "unaggregated", + ), + } } + + Ok(Some(attestation_data)) } /// Performs the second step of the attesting process: downloading an aggregated `Attestation`, @@ -453,103 +421,89 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { /// returned to the BN. async fn produce_and_publish_aggregates( &self, - attestation: Attestation<E>, + attestation_data: AttestationData, validator_duties: &[DutyAndProof], ) -> Result<(), String> { let log = self.context.log(); let aggregated_attestation = self .beacon_node - .http - .validator() - .produce_aggregate_attestation(&attestation.data) + .get_validator_aggregate_attestation( + attestation_data.slot, + attestation_data.tree_hash_root(), + ) .await - .map_err(|e| format!("Failed to produce an aggregate attestation: {:?}", e))?; + .map_err(|e| format!("Failed to produce an aggregate attestation: {:?}", e))? + .ok_or_else(|| format!("No aggregate available for {:?}", attestation_data))? + .data; - // For each validator, clone the `aggregated_attestation` and convert it into - // a `SignedAggregateAndProof` - let signed_aggregate_and_proofs = validator_duties - .iter() - .filter_map(|duty_and_proof| { - // Do not produce a signed aggregator for validators that are not + for duty_and_proof in validator_duties { + let selection_proof = if let Some(proof) = duty_and_proof.selection_proof.as_ref() { + proof + } else { + // Do not produce a signed aggregate for validators that are not // subscribed aggregators. - let selection_proof = duty_and_proof.selection_proof.as_ref()?.clone(); - - let (duty_slot, duty_committee_index, _, validator_index, _) = - duty_and_proof.attestation_duties().or_else(|| { - crit!(log, "Missing duties when signing aggregate"); - None - })?; - - let pubkey = &duty_and_proof.duty.validator_pubkey; - let slot = attestation.data.slot; - let committee_index = attestation.data.index; - - if duty_slot != slot || duty_committee_index != committee_index { - crit!(log, "Inconsistent validator duties during signing"); - return None; - } - - if let Some(signed_aggregate_and_proof) = - self.validator_store.produce_signed_aggregate_and_proof( - pubkey, - validator_index, - aggregated_attestation.clone(), - selection_proof, - ) - { - Some(signed_aggregate_and_proof) + continue; + }; + let (duty_slot, duty_committee_index, _, validator_index, _, _) = + if let Some(tuple) = duty_and_proof.attestation_duties() { + tuple } else { - crit!(log, "Failed to sign attestation"); - None - } - }) - .collect::<Vec<_>>(); + crit!(log, "Missing duties when signing aggregate"); + continue; + }; - // If there any signed aggregates and proofs were produced, publish them to the - // BN. - if let Some(first) = signed_aggregate_and_proofs.first().cloned() { - let attestation = first.message.aggregate; + let pubkey = &duty_and_proof.duty.validator_pubkey; + let slot = attestation_data.slot; + let committee_index = attestation_data.index; - let publish_status = self + if duty_slot != slot || duty_committee_index != committee_index { + crit!(log, "Inconsistent validator duties during signing"); + continue; + } + + let signed_aggregate_and_proof = if let Some(aggregate) = + self.validator_store.produce_signed_aggregate_and_proof( + pubkey, + validator_index, + aggregated_attestation.clone(), + selection_proof.clone(), + ) { + aggregate + } else { + crit!(log, "Failed to sign attestation"); + continue; + }; + + let attestation = &signed_aggregate_and_proof.message.aggregate; + + match self .beacon_node - .http - .validator() - .publish_aggregate_and_proof(signed_aggregate_and_proofs) + .post_validator_aggregate_and_proof(&signed_aggregate_and_proof) .await - .map_err(|e| format!("Failed to publish aggregate and proofs: {:?}", e))?; - match publish_status { - PublishStatus::Valid => info!( + { + Ok(()) => info!( log, - "Successfully published attestations"; + "Successfully published attestation"; + "aggregator" => signed_aggregate_and_proof.message.aggregator_index, "signatures" => attestation.aggregation_bits.num_set_bits(), "head_block" => format!("{:?}", attestation.data.beacon_block_root), "committee_index" => attestation.data.index, "slot" => attestation.data.slot.as_u64(), "type" => "aggregated", ), - PublishStatus::Invalid(msg) => crit!( + Err(e) => crit!( log, - "Published attestation was invalid"; - "message" => msg, + "Failed to publish attestation"; + "error" => e.to_string(), "committee_index" => attestation.data.index, "slot" => attestation.data.slot.as_u64(), "type" => "aggregated", ), - PublishStatus::Unknown => { - crit!(log, "Unknown condition when publishing agg. attestation") - } - }; - Ok(()) - } else { - debug!( - log, - "No signed aggregates to publish"; - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), - ); - Ok(()) + } } + + Ok(()) } } diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 60d1f4d55..bf52cacfc 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -1,19 +1,19 @@ use crate::validator_store::ValidatorStore; use environment::RuntimeContext; +use eth2::{types::Graffiti, BeaconNodeHttpClient}; use futures::channel::mpsc::Receiver; use futures::{StreamExt, TryFutureExt}; -use remote_beacon_node::{PublishStatus, RemoteBeaconNode}; use slog::{crit, debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; -use types::{EthSpec, Graffiti, PublicKey, Slot}; +use types::{EthSpec, PublicKey, Slot}; /// Builds a `BlockService`. pub struct BlockServiceBuilder<T, E: EthSpec> { validator_store: Option<ValidatorStore<T, E>>, slot_clock: Option<Arc<T>>, - beacon_node: Option<RemoteBeaconNode<E>>, + beacon_node: Option<BeaconNodeHttpClient>, context: Option<RuntimeContext<E>>, graffiti: Option<Graffiti>, } @@ -39,7 +39,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> { self } - pub fn beacon_node(mut self, beacon_node: RemoteBeaconNode<E>) -> Self { + pub fn beacon_node(mut self, beacon_node: BeaconNodeHttpClient) -> Self { self.beacon_node = Some(beacon_node); self } @@ -79,7 +79,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> { pub struct Inner<T, E: EthSpec> { validator_store: ValidatorStore<T, E>, slot_clock: Arc<T>, - beacon_node: RemoteBeaconNode<E>, + beacon_node: BeaconNodeHttpClient, context: RuntimeContext<E>, graffiti: Option<Graffiti>, } @@ -221,41 +221,28 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { let block = self .beacon_node - .http - .validator() - .produce_block(slot, randao_reveal, self.graffiti) + .get_validator_blocks(slot, randao_reveal.into(), self.graffiti.as_ref()) .await - .map_err(|e| format!("Error from beacon node when producing block: {:?}", e))?; + .map_err(|e| format!("Error from beacon node when producing block: {:?}", e))? + .data; let signed_block = self .validator_store .sign_block(&validator_pubkey, block, current_slot) .ok_or_else(|| "Unable to sign block".to_string())?; - let publish_status = self - .beacon_node - .http - .validator() - .publish_block(signed_block.clone()) + self.beacon_node + .post_beacon_blocks(&signed_block) .await .map_err(|e| format!("Error from beacon node when publishing block: {:?}", e))?; - match publish_status { - PublishStatus::Valid => info!( - log, - "Successfully published block"; - "deposits" => signed_block.message.body.deposits.len(), - "attestations" => signed_block.message.body.attestations.len(), - "slot" => signed_block.slot().as_u64(), - ), - PublishStatus::Invalid(msg) => crit!( - log, - "Published block was invalid"; - "message" => msg, - "slot" => signed_block.slot().as_u64(), - ), - PublishStatus::Unknown => crit!(log, "Unknown condition when publishing block"), - } + info!( + log, + "Successfully published block"; + "deposits" => signed_block.message.body.deposits.len(), + "attestations" => signed_block.message.body.attestations.len(), + "slot" => signed_block.slot().as_u64(), + ); Ok(()) } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 991b55162..4d230b1b4 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -4,9 +4,10 @@ use directory::{ get_testnet_name, DEFAULT_HARDCODED_TESTNET, DEFAULT_ROOT_DIR, DEFAULT_SECRET_DIR, DEFAULT_VALIDATOR_DIR, }; +use eth2::types::Graffiti; use serde_derive::{Deserialize, Serialize}; use std::path::PathBuf; -use types::{Graffiti, GRAFFITI_BYTES_LEN}; +use types::GRAFFITI_BYTES_LEN; pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/"; /// Path to the slashing protection database within the datadir. @@ -119,15 +120,14 @@ impl Config { GRAFFITI_BYTES_LEN )); } else { - // Default graffiti to all 0 bytes. - let mut graffiti = Graffiti::default(); + let mut graffiti = [0; 32]; // Copy the provided bytes over. // // Panic-free because `graffiti_bytes.len()` <= `GRAFFITI_BYTES_LEN`. graffiti[..graffiti_bytes.len()].copy_from_slice(&graffiti_bytes); - config.graffiti = Some(graffiti); + config.graffiti = Some(graffiti.into()); } } diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 7375d5502..7f6d33fe8 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -1,16 +1,15 @@ use crate::{ - block_service::BlockServiceNotification, is_synced::is_synced, validator_store::ValidatorStore, + block_service::BlockServiceNotification, is_synced::is_synced, validator_duty::ValidatorDuty, + validator_store::ValidatorStore, }; use environment::RuntimeContext; +use eth2::BeaconNodeHttpClient; use futures::channel::mpsc::Sender; use futures::{SinkExt, StreamExt}; use parking_lot::RwLock; -use remote_beacon_node::{PublishStatus, RemoteBeaconNode}; -use rest_types::{ValidatorDuty, ValidatorDutyBytes, ValidatorSubscription}; use slog::{debug, error, trace, warn}; use slot_clock::SlotClock; use std::collections::HashMap; -use std::convert::TryInto; use std::ops::Deref; use std::sync::Arc; use tokio::time::{interval_at, Duration, Instant}; @@ -44,14 +43,14 @@ impl DutyAndProof { pub fn compute_selection_proof<T: SlotClock + 'static, E: EthSpec>( &mut self, validator_store: &ValidatorStore<T, E>, + spec: &ChainSpec, ) -> Result<(), String> { - let (modulo, slot) = if let (Some(modulo), Some(slot)) = - (self.duty.aggregator_modulo, self.duty.attestation_slot) + let (committee_length, slot) = if let (Some(count), Some(slot)) = + (self.duty.committee_length, self.duty.attestation_slot) { - (modulo, slot) + (count as usize, slot) } else { - // If there is no modulo or for the aggregator we assume they are not activated and - // therefore not an aggregator. + // If there are no attester duties we assume the validator is inactive. self.selection_proof = None; return Ok(()); }; @@ -61,7 +60,7 @@ impl DutyAndProof { .ok_or_else(|| "Failed to produce selection proof".to_string())?; self.selection_proof = selection_proof - .is_aggregator_from_modulo(modulo) + .is_aggregator(committee_length, spec) .map_err(|e| format!("Invalid modulo: {:?}", e)) .map(|is_aggregator| { if is_aggregator { @@ -87,19 +86,20 @@ impl DutyAndProof { /// It's important to note that this doesn't actually check `self.selection_proof`, instead it /// checks to see if the inputs to computing the selection proof are equal. fn selection_proof_eq(&self, other: &Self) -> bool { - self.duty.aggregator_modulo == other.duty.aggregator_modulo + self.duty.committee_count_at_slot == other.duty.committee_count_at_slot && self.duty.attestation_slot == other.duty.attestation_slot } /// Returns the information required for an attesting validator, if they are scheduled to /// attest. - pub fn attestation_duties(&self) -> Option<(Slot, CommitteeIndex, usize, u64, u64)> { + pub fn attestation_duties(&self) -> Option<(Slot, CommitteeIndex, usize, u64, u64, u64)> { Some(( self.duty.attestation_slot?, self.duty.attestation_committee_index?, self.duty.attestation_committee_position?, self.duty.validator_index?, self.duty.committee_count_at_slot?, + self.duty.committee_length?, )) } @@ -108,26 +108,12 @@ impl DutyAndProof { } } -impl TryInto<DutyAndProof> for ValidatorDutyBytes { - type Error = String; - - fn try_into(self) -> Result<DutyAndProof, Self::Error> { - let duty = ValidatorDuty { - validator_pubkey: (&self.validator_pubkey) - .try_into() - .map_err(|e| format!("Invalid pubkey bytes from server: {:?}", e))?, - validator_index: self.validator_index, - attestation_slot: self.attestation_slot, - attestation_committee_index: self.attestation_committee_index, - attestation_committee_position: self.attestation_committee_position, - committee_count_at_slot: self.committee_count_at_slot, - block_proposal_slots: self.block_proposal_slots, - aggregator_modulo: self.aggregator_modulo, - }; - Ok(DutyAndProof { - duty, +impl Into<DutyAndProof> for ValidatorDuty { + fn into(self) -> DutyAndProof { + DutyAndProof { + duty: self, selection_proof: None, - }) + } } } @@ -260,6 +246,7 @@ impl DutiesStore { mut duties: DutyAndProof, slots_per_epoch: u64, validator_store: &ValidatorStore<T, E>, + spec: &ChainSpec, ) -> Result<InsertOutcome, String> { let mut store = self.store.write(); @@ -282,7 +269,7 @@ impl DutiesStore { } } else { // Compute the selection proof. - duties.compute_selection_proof(validator_store)?; + duties.compute_selection_proof(validator_store, spec)?; // Determine if a re-subscription is required. let should_resubscribe = !duties.subscription_eq(known_duties); @@ -294,7 +281,7 @@ impl DutiesStore { } } else { // Compute the selection proof. - duties.compute_selection_proof(validator_store)?; + duties.compute_selection_proof(validator_store, spec)?; validator_map.insert(epoch, duties); @@ -302,7 +289,7 @@ impl DutiesStore { } } else { // Compute the selection proof. - duties.compute_selection_proof(validator_store)?; + duties.compute_selection_proof(validator_store, spec)?; let validator_pubkey = duties.duty.validator_pubkey.clone(); @@ -328,7 +315,7 @@ impl DutiesStore { pub struct DutiesServiceBuilder<T, E: EthSpec> { validator_store: Option<ValidatorStore<T, E>>, slot_clock: Option<T>, - beacon_node: Option<RemoteBeaconNode<E>>, + beacon_node: Option<BeaconNodeHttpClient>, context: Option<RuntimeContext<E>>, allow_unsynced_beacon_node: bool, } @@ -354,7 +341,7 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesServiceBuilder<T, E> { self } - pub fn beacon_node(mut self, beacon_node: RemoteBeaconNode<E>) -> Self { + pub fn beacon_node(mut self, beacon_node: BeaconNodeHttpClient) -> Self { self.beacon_node = Some(beacon_node); self } @@ -397,7 +384,7 @@ pub struct Inner<T, E: EthSpec> { store: Arc<DutiesStore>, validator_store: ValidatorStore<T, E>, pub(crate) slot_clock: T, - pub(crate) beacon_node: RemoteBeaconNode<E>, + pub(crate) beacon_node: BeaconNodeHttpClient, context: RuntimeContext<E>, /// If true, the duties service will poll for duties from the beacon node even if it is not /// synced. @@ -462,7 +449,7 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { pub fn start_update_service( self, mut block_service_tx: Sender<BlockServiceNotification>, - spec: &ChainSpec, + spec: Arc<ChainSpec>, ) -> Result<(), String> { let duration_to_next_slot = self .slot_clock @@ -481,17 +468,22 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { // Run an immediate update before starting the updater service. let duties_service = self.clone(); let mut block_service_tx_clone = block_service_tx.clone(); + let inner_spec = spec.clone(); self.inner .context .executor .runtime_handle() - .spawn(async move { duties_service.do_update(&mut block_service_tx_clone).await }); + .spawn(async move { + duties_service + .do_update(&mut block_service_tx_clone, &inner_spec) + .await + }); let executor = self.inner.context.executor.clone(); let interval_fut = async move { while interval.next().await.is_some() { - self.clone().do_update(&mut block_service_tx).await; + self.clone().do_update(&mut block_service_tx, &spec).await; } }; @@ -501,7 +493,11 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { } /// Attempt to download the duties of all managed validators for this epoch and the next. - async fn do_update(self, block_service_tx: &mut Sender<BlockServiceNotification>) { + async fn do_update( + self, + block_service_tx: &mut Sender<BlockServiceNotification>, + spec: &ChainSpec, + ) { let log = self.context.log(); if !is_synced(&self.beacon_node, &self.slot_clock, None).await @@ -534,7 +530,11 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { // Update duties for the current epoch, but keep running if there's an error: // block production or the next epoch update could still succeed. - if let Err(e) = self.clone().update_epoch(current_epoch).await { + if let Err(e) = self + .clone() + .update_epoch(current_epoch, current_epoch, spec) + .await + { error!( log, "Failed to get current epoch duties"; @@ -558,7 +558,11 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { }; // Update duties for the next epoch. - if let Err(e) = self.clone().update_epoch(current_epoch + 1).await { + if let Err(e) = self + .clone() + .update_epoch(current_epoch, current_epoch + 1, spec) + .await + { error!( log, "Failed to get next epoch duties"; @@ -567,18 +571,15 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { } } - /// Attempt to download the duties of all managed validators for the given `epoch`. - async fn update_epoch(self, epoch: Epoch) -> Result<(), String> { - let pubkeys = self.validator_store.voting_pubkeys(); - let all_duties = self - .beacon_node - .http - .validator() - .get_duties(epoch, pubkeys.as_slice()) - .await - .map_err(move |e| format!("Failed to get duties for epoch {}: {:?}", epoch, e))?; - - let log = self.context.log().clone(); + /// Attempt to download the duties of all managed validators for the given `request_epoch`. The + /// `current_epoch` should be a local reading of the slot clock. + async fn update_epoch( + self, + current_epoch: Epoch, + request_epoch: Epoch, + spec: &ChainSpec, + ) -> Result<(), String> { + let log = self.context.log(); let mut new_validator = 0; let mut new_epoch = 0; @@ -587,74 +588,76 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { let mut replaced = 0; let mut invalid = 0; - // For each of the duties, attempt to insert them into our local store and build a - // list of new or changed selections proofs for any aggregating validators. - let validator_subscriptions = all_duties - .into_iter() - .filter_map(|remote_duties| { - // Convert the remote duties into our local representation. - let duties: DutyAndProof = remote_duties - .clone() - .try_into() - .map_err(|e| { - error!( - log, - "Unable to convert remote duties"; - "error" => e - ) - }) - .ok()?; - - let validator_pubkey = duties.duty.validator_pubkey.clone(); - - // Attempt to update our local store. - let outcome = self - .store - .insert(epoch, duties, E::slots_per_epoch(), &self.validator_store) - .map_err(|e| { - error!( - log, - "Unable to store duties"; - "error" => e - ) - }) - .ok()?; - - match &outcome { - InsertOutcome::NewValidator => { - debug!( - log, - "First duty assignment for validator"; - "proposal_slots" => format!("{:?}", &remote_duties.block_proposal_slots), - "attestation_slot" => format!("{:?}", &remote_duties.attestation_slot), - "validator" => format!("{:?}", &remote_duties.validator_pubkey) - ); - new_validator += 1; - } - InsertOutcome::NewProposalSlots => new_proposal_slots += 1, - InsertOutcome::NewEpoch => new_epoch += 1, - InsertOutcome::Identical => identical += 1, - InsertOutcome::Replaced { .. } => replaced += 1, - InsertOutcome::Invalid => invalid += 1, - }; - - // The selection proof is computed on `store.insert`, so it's necessary to check - // with the store that the validator is an aggregator. - let is_aggregator = self.store.is_aggregator(&validator_pubkey, epoch)?; - - if outcome.is_subscription_candidate() { - Some(ValidatorSubscription { - validator_index: remote_duties.validator_index?, - attestation_committee_index: remote_duties.attestation_committee_index?, - slot: remote_duties.attestation_slot?, - committee_count_at_slot: remote_duties.committee_count_at_slot?, - is_aggregator, - }) - } else { - None + let mut validator_subscriptions = vec![]; + for pubkey in self.validator_store.voting_pubkeys() { + let remote_duties = match ValidatorDuty::download( + &self.beacon_node, + current_epoch, + request_epoch, + pubkey, + ) + .await + { + Ok(duties) => duties, + Err(e) => { + error!( + log, + "Failed to download validator duties"; + "error" => e + ); + continue; } - }) - .collect::<Vec<_>>(); + }; + + // Convert the remote duties into our local representation. + let duties: DutyAndProof = remote_duties.clone().into(); + + let validator_pubkey = duties.duty.validator_pubkey.clone(); + + // Attempt to update our local store. + match self.store.insert( + request_epoch, + duties, + E::slots_per_epoch(), + &self.validator_store, + spec, + ) { + Ok(outcome) => { + match &outcome { + InsertOutcome::NewValidator => { + debug!( + log, + "First duty assignment for validator"; + "proposal_slots" => format!("{:?}", &remote_duties.block_proposal_slots), + "attestation_slot" => format!("{:?}", &remote_duties.attestation_slot), + "validator" => format!("{:?}", &remote_duties.validator_pubkey) + ); + new_validator += 1; + } + InsertOutcome::NewProposalSlots => new_proposal_slots += 1, + InsertOutcome::NewEpoch => new_epoch += 1, + InsertOutcome::Identical => identical += 1, + InsertOutcome::Replaced { .. } => replaced += 1, + InsertOutcome::Invalid => invalid += 1, + } + + if let Some(is_aggregator) = + self.store.is_aggregator(&validator_pubkey, request_epoch) + { + if outcome.is_subscription_candidate() { + if let Some(subscription) = remote_duties.subscription(is_aggregator) { + validator_subscriptions.push(subscription) + } + } + } + } + Err(e) => error!( + log, + "Unable to store duties"; + "error" => e + ), + } + } if invalid > 0 { error!( @@ -673,7 +676,7 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { "new_proposal_slots" => new_proposal_slots, "new_validator" => new_validator, "replaced" => replaced, - "epoch" => format!("{}", epoch) + "epoch" => format!("{}", request_epoch) ); if replaced > 0 { @@ -690,34 +693,19 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> { if count == 0 { debug!(log, "No new subscriptions required"); - - Ok(()) } else { self.beacon_node - .http - .validator() - .subscribe(validator_subscriptions) + .post_validator_beacon_committee_subscriptions(&validator_subscriptions) .await - .map_err(|e| format!("Failed to subscribe validators: {:?}", e)) - .map(move |status| { - match status { - PublishStatus::Valid => debug!( - log, - "Successfully subscribed validators"; - "count" => count - ), - PublishStatus::Unknown => error!( - log, - "Unknown response from subscription"; - ), - PublishStatus::Invalid(e) => error!( - log, - "Failed to subscribe validator"; - "error" => e - ), - }; - }) + .map_err(|e| format!("Failed to subscribe validators: {:?}", e))?; + debug!( + log, + "Successfully subscribed validators"; + "count" => count + ); } + + Ok(()) } } diff --git a/validator_client/src/fork_service.rs b/validator_client/src/fork_service.rs index b8db7b72e..e38a4cf3c 100644 --- a/validator_client/src/fork_service.rs +++ b/validator_client/src/fork_service.rs @@ -1,7 +1,7 @@ use environment::RuntimeContext; +use eth2::{types::StateId, BeaconNodeHttpClient}; use futures::StreamExt; use parking_lot::RwLock; -use remote_beacon_node::RemoteBeaconNode; use slog::{debug, trace}; use slot_clock::SlotClock; use std::ops::Deref; @@ -16,7 +16,7 @@ const TIME_DELAY_FROM_SLOT: Duration = Duration::from_millis(80); pub struct ForkServiceBuilder<T, E: EthSpec> { fork: Option<Fork>, slot_clock: Option<T>, - beacon_node: Option<RemoteBeaconNode<E>>, + beacon_node: Option<BeaconNodeHttpClient>, context: Option<RuntimeContext<E>>, } @@ -35,7 +35,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkServiceBuilder<T, E> { self } - pub fn beacon_node(mut self, beacon_node: RemoteBeaconNode<E>) -> Self { + pub fn beacon_node(mut self, beacon_node: BeaconNodeHttpClient) -> Self { self.beacon_node = Some(beacon_node); self } @@ -66,7 +66,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkServiceBuilder<T, E> { /// Helper to minimise `Arc` usage. pub struct Inner<T, E: EthSpec> { fork: RwLock<Option<Fork>>, - beacon_node: RemoteBeaconNode<E>, + beacon_node: BeaconNodeHttpClient, context: RuntimeContext<E>, slot_clock: T, } @@ -141,9 +141,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { let fork = self .inner .beacon_node - .http - .beacon() - .get_fork() + .get_beacon_states_fork(StateId::Head) .await .map_err(|e| { trace!( @@ -151,7 +149,15 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { "Fork update failed"; "error" => format!("Error retrieving fork: {:?}", e) ) - })?; + })? + .ok_or_else(|| { + trace!( + log, + "Fork update failed"; + "error" => "The beacon head fork is unknown" + ) + })? + .data; if self.fork.read().as_ref() != Some(&fork) { *(self.fork.write()) = Some(fork); diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 400768f5c..a097d7245 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -50,8 +50,6 @@ pub enum Error { UnableToSaveDefinitions(validator_definitions::Error), /// It is not legal to try and initialize a disabled validator definition. UnableToInitializeDisabledValidator, - /// It is not legal to try and initialize a disabled validator definition. - PasswordUnknown(PathBuf), /// There was an error reading from stdin. UnableToReadPasswordFromUser(String), /// There was an error running a tokio async task. @@ -333,6 +331,7 @@ impl InitializedValidators { /// validator will be removed from `self.validators`. /// /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. + #[allow(dead_code)] // Will be used once VC API is enabled. pub async fn set_validator_status( &mut self, voting_public_key: &PublicKey, diff --git a/validator_client/src/is_synced.rs b/validator_client/src/is_synced.rs index e1017ac77..f967d629c 100644 --- a/validator_client/src/is_synced.rs +++ b/validator_client/src/is_synced.rs @@ -1,8 +1,6 @@ -use remote_beacon_node::RemoteBeaconNode; -use rest_types::SyncingResponse; -use slog::{debug, error, Logger}; +use eth2::BeaconNodeHttpClient; +use slog::{debug, error, warn, Logger}; use slot_clock::SlotClock; -use types::EthSpec; /// A distance in slots. const SYNC_TOLERANCE: u64 = 4; @@ -17,19 +15,19 @@ const SYNC_TOLERANCE: u64 = 4; /// /// The second condition means the even if the beacon node thinks that it's syncing, we'll still /// try to use it if it's close enough to the head. -pub async fn is_synced<T: SlotClock, E: EthSpec>( - beacon_node: &RemoteBeaconNode<E>, +pub async fn is_synced<T: SlotClock>( + beacon_node: &BeaconNodeHttpClient, slot_clock: &T, log_opt: Option<&Logger>, ) -> bool { - let resp = match beacon_node.http.node().syncing_status().await { + let resp = match beacon_node.get_node_syncing().await { Ok(resp) => resp, Err(e) => { if let Some(log) = log_opt { error!( log, "Unable connect to beacon node"; - "error" => format!("{:?}", e) + "error" => e.to_string() ) } @@ -37,44 +35,38 @@ pub async fn is_synced<T: SlotClock, E: EthSpec>( } }; - match &resp { - SyncingResponse { - is_syncing: false, .. - } => true, - SyncingResponse { - is_syncing: true, - sync_status, - } => { - if let Some(log) = log_opt { - debug!( + let is_synced = !resp.data.is_syncing || (resp.data.sync_distance.as_u64() < SYNC_TOLERANCE); + + if let Some(log) = log_opt { + if !is_synced { + debug!( + log, + "Beacon node sync status"; + "status" => format!("{:?}", resp), + ); + + warn!( + log, + "Beacon node is syncing"; + "msg" => "not receiving new duties", + "sync_distance" => resp.data.sync_distance.as_u64(), + "head_slot" => resp.data.head_slot.as_u64(), + ); + } + + if let Some(local_slot) = slot_clock.now() { + let remote_slot = resp.data.head_slot + resp.data.sync_distance; + if remote_slot + 1 < local_slot || local_slot + 1 < remote_slot { + error!( log, - "Beacon node sync status"; - "status" => format!("{:?}", resp), + "Time discrepancy with beacon node"; + "msg" => "check the system time on this host and the beacon node", + "beacon_node_slot" => remote_slot, + "local_slot" => local_slot, ); } - - let now = if let Some(slot) = slot_clock.now() { - slot - } else { - // There's no good reason why we shouldn't be able to read the slot clock, so we'll - // indicate we're not synced if that's the case. - return false; - }; - - if sync_status.current_slot + SYNC_TOLERANCE >= now { - true - } else { - if let Some(log) = log_opt { - error!( - log, - "Beacon node is syncing"; - "msg" => "not receiving new duties", - "target_slot" => sync_status.highest_slot.as_u64(), - "current_slot" => sync_status.current_slot.as_u64(), - ); - } - false - } } } + + is_synced } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 6d82baa6b..8a0e8ba1e 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -7,6 +7,7 @@ mod fork_service; mod initialized_validators; mod is_synced; mod notifier; +mod validator_duty; mod validator_store; pub use cli::cli_app; @@ -18,18 +19,18 @@ use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; use duties_service::{DutiesService, DutiesServiceBuilder}; use environment::RuntimeContext; -use eth2_config::Eth2Config; +use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Url}; use fork_service::{ForkService, ForkServiceBuilder}; use futures::channel::mpsc; use initialized_validators::InitializedValidators; use notifier::spawn_notifier; -use remote_beacon_node::RemoteBeaconNode; use slog::{error, info, Logger}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{delay_for, Duration}; -use types::{EthSpec, Hash256}; +use types::{EthSpec, Hash256, YamlConfig}; use validator_store::ValidatorStore; /// The interval between attempts to contact the beacon node during startup. @@ -61,7 +62,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. - pub async fn new(mut context: RuntimeContext<T>, config: Config) -> Result<Self, String> { + pub async fn new(context: RuntimeContext<T>, config: Config) -> Result<Self, String> { let log = context.log().clone(); info!( @@ -104,33 +105,36 @@ impl<T: EthSpec> ProductionValidatorClient<T> { "enabled" => validators.num_enabled(), ); + let beacon_node_url: Url = config + .http_server + .parse() + .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; + let beacon_node_http_client = ClientBuilder::new() + .timeout(HTTP_TIMEOUT) + .build() + .map_err(|e| format!("Unable to build HTTP client: {:?}", e))?; let beacon_node = - RemoteBeaconNode::new_with_timeout(config.http_server.clone(), HTTP_TIMEOUT) - .map_err(|e| format!("Unable to init beacon node http client: {}", e))?; + BeaconNodeHttpClient::from_components(beacon_node_url, beacon_node_http_client); // Perform some potentially long-running initialization tasks. - let (eth2_config, genesis_time, genesis_validators_root) = tokio::select! { + let (yaml_config, genesis_time, genesis_validators_root) = tokio::select! { tuple = init_from_beacon_node(&beacon_node, &context) => tuple?, () = context.executor.exit() => return Err("Shutting down".to_string()) }; + let beacon_node_spec = yaml_config.apply_to_chain_spec::<T>(&T::default_spec()) + .ok_or_else(|| + "The minimal/mainnet spec type of the beacon node does not match the validator client. \ + See the --testnet command.".to_string() + )?; - // Do not permit a connection to a beacon node using different spec constants. - if context.eth2_config.spec_constants != eth2_config.spec_constants { - return Err(format!( - "Beacon node is using an incompatible spec. Got {}, expected {}", - eth2_config.spec_constants, context.eth2_config.spec_constants - )); + if context.eth2_config.spec != beacon_node_spec { + return Err( + "The beacon node is using a different Eth2 specification to this validator client. \ + See the --testnet command." + .to_string(), + ); } - // Note: here we just assume the spec variables of the remote node. This is very useful - // for testnets, but perhaps a security issue when it comes to mainnet. - // - // A damaging attack would be for a beacon node to convince the validator client of a - // different `SLOTS_PER_EPOCH` variable. This could result in slashable messages being - // produced. We are safe from this because `SLOTS_PER_EPOCH` is a type-level constant - // for Lighthouse. - context.eth2_config = eth2_config; - let slot_clock = SystemTimeSlotClock::new( context.eth2_config.spec.genesis_slot, Duration::from_secs(genesis_time), @@ -203,7 +207,10 @@ impl<T: EthSpec> ProductionValidatorClient<T> { self.duties_service .clone() - .start_update_service(block_service_tx, &self.context.eth2_config.spec) + .start_update_service( + block_service_tx, + Arc::new(self.context.eth2_config.spec.clone()), + ) .map_err(|e| format!("Unable to start duties service: {}", e))?; self.fork_service @@ -228,80 +235,85 @@ impl<T: EthSpec> ProductionValidatorClient<T> { } async fn init_from_beacon_node<E: EthSpec>( - beacon_node: &RemoteBeaconNode<E>, + beacon_node: &BeaconNodeHttpClient, context: &RuntimeContext<E>, -) -> Result<(Eth2Config, u64, Hash256), String> { +) -> Result<(YamlConfig, u64, Hash256), String> { // Wait for the beacon node to come online. wait_for_node(beacon_node, context.log()).await?; - let eth2_config = beacon_node - .http - .spec() - .get_eth2_config() + let yaml_config = beacon_node + .get_config_spec() .await - .map_err(|e| format!("Unable to read eth2 config from beacon node: {:?}", e))?; - let genesis_time = beacon_node - .http - .beacon() - .get_genesis_time() - .await - .map_err(|e| format!("Unable to read genesis time from beacon node: {:?}", e))?; + .map_err(|e| format!("Unable to read spec from beacon node: {:?}", e))? + .data; + + let genesis = loop { + match beacon_node.get_beacon_genesis().await { + Ok(genesis) => break genesis.data, + Err(e) => { + // A 404 error on the genesis endpoint indicates that genesis has not yet occurred. + if e.status() == Some(StatusCode::NOT_FOUND) { + info!( + context.log(), + "Waiting for genesis"; + ); + } else { + error!( + context.log(), + "Error polling beacon node"; + "error" => format!("{:?}", e) + ); + } + } + } + + delay_for(RETRY_DELAY).await; + }; + let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| format!("Unable to read system time: {:?}", e))?; - let genesis = Duration::from_secs(genesis_time); + let genesis_time = Duration::from_secs(genesis.genesis_time); // If the time now is less than (prior to) genesis, then delay until the // genesis instant. // // If the validator client starts before genesis, it will get errors from // the slot clock. - if now < genesis { + if now < genesis_time { info!( context.log(), "Starting node prior to genesis"; - "seconds_to_wait" => (genesis - now).as_secs() + "seconds_to_wait" => (genesis_time - now).as_secs() ); - delay_for(genesis - now).await; + delay_for(genesis_time - now).await; } else { info!( context.log(), "Genesis has already occurred"; - "seconds_ago" => (now - genesis).as_secs() + "seconds_ago" => (now - genesis_time).as_secs() ); } - let genesis_validators_root = beacon_node - .http - .beacon() - .get_genesis_validators_root() - .await - .map_err(|e| { - format!( - "Unable to read genesis validators root from beacon node: {:?}", - e - ) - })?; - Ok((eth2_config, genesis_time, genesis_validators_root)) + Ok(( + yaml_config, + genesis.genesis_time, + genesis.genesis_validators_root, + )) } /// Request the version from the node, looping back and trying again on failure. Exit once the node /// has been contacted. -async fn wait_for_node<E: EthSpec>( - beacon_node: &RemoteBeaconNode<E>, - log: &Logger, -) -> Result<(), String> { +async fn wait_for_node(beacon_node: &BeaconNodeHttpClient, log: &Logger) -> Result<(), String> { // Try to get the version string from the node, looping until success is returned. loop { let log = log.clone(); let result = beacon_node - .clone() - .http - .node() - .get_version() + .get_node_version() .await - .map_err(|e| format!("{:?}", e)); + .map_err(|e| format!("{:?}", e)) + .map(|body| body.data.version); match result { Ok(version) => { diff --git a/validator_client/src/validator_duty.rs b/validator_client/src/validator_duty.rs new file mode 100644 index 000000000..e5f56c385 --- /dev/null +++ b/validator_client/src/validator_duty.rs @@ -0,0 +1,131 @@ +use eth2::{ + types::{BeaconCommitteeSubscription, StateId, ValidatorId}, + BeaconNodeHttpClient, +}; +use serde::{Deserialize, Serialize}; +use types::{CommitteeIndex, Epoch, PublicKey, PublicKeyBytes, Slot}; + +/// This struct is being used as a shim since we deprecated the `rest_api` in favour of `http_api`. +/// +/// Tracking issue: https://github.com/sigp/lighthouse/issues/1643 +// NOTE: if you add or remove fields, please adjust `eq_ignoring_proposal_slots` +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +pub struct ValidatorDuty { + /// The validator's BLS public key, uniquely identifying them. + pub validator_pubkey: PublicKey, + /// The validator's index in `state.validators` + pub validator_index: Option<u64>, + /// The slot at which the validator must attest. + pub attestation_slot: Option<Slot>, + /// The index of the committee within `slot` of which the validator is a member. + pub attestation_committee_index: Option<CommitteeIndex>, + /// The position of the validator in the committee. + pub attestation_committee_position: Option<usize>, + /// The committee count at `attestation_slot`. + pub committee_count_at_slot: Option<u64>, + /// The number of validators in the committee. + pub committee_length: Option<u64>, + /// The slots in which a validator must propose a block (can be empty). + /// + /// Should be set to `None` when duties are not yet known (before the current epoch). + pub block_proposal_slots: Option<Vec<Slot>>, +} + +impl ValidatorDuty { + /// Instantiate `Self` as if there are no known dutes for `validator_pubkey`. + fn no_duties(validator_pubkey: PublicKey) -> Self { + ValidatorDuty { + validator_pubkey, + validator_index: None, + attestation_slot: None, + attestation_committee_index: None, + attestation_committee_position: None, + committee_count_at_slot: None, + committee_length: None, + block_proposal_slots: None, + } + } + + /// Instantiate `Self` by performing requests on the `beacon_node`. + /// + /// Will only request proposer duties if `current_epoch == request_epoch`. + pub async fn download( + beacon_node: &BeaconNodeHttpClient, + current_epoch: Epoch, + request_epoch: Epoch, + pubkey: PublicKey, + ) -> Result<ValidatorDuty, String> { + let pubkey_bytes = PublicKeyBytes::from(&pubkey); + + let validator_index = if let Some(index) = beacon_node + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(pubkey_bytes.clone()), + ) + .await + .map_err(|e| format!("Failed to get validator index: {}", e))? + .map(|body| body.data.index) + { + index + } else { + return Ok(Self::no_duties(pubkey)); + }; + + if let Some(attester) = beacon_node + .get_validator_duties_attester(request_epoch, Some(&[validator_index])) + .await + .map_err(|e| format!("Failed to get attester duties: {}", e))? + .data + .first() + { + let block_proposal_slots = if current_epoch == request_epoch { + beacon_node + .get_validator_duties_proposer(current_epoch) + .await + .map_err(|e| format!("Failed to get proposer indices: {}", e))? + .data + .into_iter() + .filter(|data| data.pubkey == pubkey_bytes) + .map(|data| data.slot) + .collect() + } else { + vec![] + }; + + Ok(ValidatorDuty { + validator_pubkey: pubkey, + validator_index: Some(attester.validator_index), + attestation_slot: Some(attester.slot), + attestation_committee_index: Some(attester.committee_index), + attestation_committee_position: Some(attester.validator_committee_index as usize), + committee_count_at_slot: Some(attester.committees_at_slot), + committee_length: Some(attester.committee_length), + block_proposal_slots: Some(block_proposal_slots), + }) + } else { + Ok(Self::no_duties(pubkey)) + } + } + + /// Return `true` if these validator duties are equal, ignoring their `block_proposal_slots`. + pub fn eq_ignoring_proposal_slots(&self, other: &Self) -> bool { + self.validator_pubkey == other.validator_pubkey + && self.validator_index == other.validator_index + && self.attestation_slot == other.attestation_slot + && self.attestation_committee_index == other.attestation_committee_index + && self.attestation_committee_position == other.attestation_committee_position + && self.committee_count_at_slot == other.committee_count_at_slot + && self.committee_length == other.committee_length + } + + /// Generate a subscription for `self`, if `self` has appropriate attestation duties. + pub fn subscription(&self, is_aggregator: bool) -> Option<BeaconCommitteeSubscription> { + Some(BeaconCommitteeSubscription { + validator_index: self.validator_index?, + committee_index: self.attestation_committee_index?, + committees_at_slot: self.committee_count_at_slot?, + slot: self.attestation_slot?, + is_aggregator, + }) + } +} From 22aedda1bec23b45eddf71c55e898fc9792acee6 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michael@sigmaprime.io> Date: Wed, 30 Sep 2020 02:36:07 +0000 Subject: [PATCH 05/32] Add database schema versioning (#1688) ## Issue Addressed Closes #673 ## Proposed Changes Store a schema version in the database so that future releases can check they're running against a compatible database version. This would also enable automatic migration on breaking database changes, but that's left as future work. The database config is also stored in the database so that the `slots_per_restore_point` value can be checked for consistency, which closes #673 --- beacon_node/beacon_chain/src/beacon_chain.rs | 22 ++++--- beacon_node/beacon_chain/src/builder.rs | 10 ++-- beacon_node/beacon_chain/tests/tests.rs | 3 +- beacon_node/network/src/persisted_dht.rs | 18 +++--- beacon_node/src/cli.rs | 3 +- beacon_node/store/src/config.rs | 36 ++++++++++- beacon_node/store/src/errors.rs | 8 +++ beacon_node/store/src/hot_cold_store.rs | 63 +++++++++++++++++--- beacon_node/store/src/lib.rs | 3 +- beacon_node/store/src/metadata.rs | 29 +++++++++ 10 files changed, 153 insertions(+), 42 deletions(-) create mode 100644 beacon_node/store/src/metadata.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 3bf5ae282..d189b01e2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -66,10 +66,11 @@ pub const ATTESTATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1); /// validator pubkey cache. pub const VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1); -pub const BEACON_CHAIN_DB_KEY: [u8; 32] = [0; 32]; -pub const OP_POOL_DB_KEY: [u8; 32] = [0; 32]; -pub const ETH1_CACHE_DB_KEY: [u8; 32] = [0; 32]; -pub const FORK_CHOICE_DB_KEY: [u8; 32] = [0; 32]; +// These keys are all zero because they get stored in different columns, see `DBColumn` type. +pub const BEACON_CHAIN_DB_KEY: Hash256 = Hash256::zero(); +pub const OP_POOL_DB_KEY: Hash256 = Hash256::zero(); +pub const ETH1_CACHE_DB_KEY: Hash256 = Hash256::zero(); +pub const FORK_CHOICE_DB_KEY: Hash256 = Hash256::zero(); /// The result of a chain segment processing. pub enum ChainSegmentResult<T: EthSpec> { @@ -260,7 +261,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let fork_choice = self.fork_choice.read(); self.store.put_item( - &Hash256::from_slice(&FORK_CHOICE_DB_KEY), + &FORK_CHOICE_DB_KEY, &PersistedForkChoice { fork_choice: fork_choice.to_persisted(), fork_choice_store: fork_choice.fc_store().to_persisted(), @@ -272,8 +273,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { metrics::stop_timer(fork_choice_timer); let head_timer = metrics::start_timer(&metrics::PERSIST_HEAD); - self.store - .put_item(&Hash256::from_slice(&BEACON_CHAIN_DB_KEY), &persisted_head)?; + self.store.put_item(&BEACON_CHAIN_DB_KEY, &persisted_head)?; metrics::stop_timer(head_timer); @@ -290,7 +290,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let _timer = metrics::start_timer(&metrics::PERSIST_OP_POOL); self.store.put_item( - &Hash256::from_slice(&OP_POOL_DB_KEY), + &OP_POOL_DB_KEY, &PersistedOperationPool::from_operation_pool(&self.op_pool), )?; @@ -302,10 +302,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let _timer = metrics::start_timer(&metrics::PERSIST_OP_POOL); if let Some(eth1_chain) = self.eth1_chain.as_ref() { - self.store.put_item( - &Hash256::from_slice(Ð1_CACHE_DB_KEY), - ð1_chain.as_ssz_container(), - )?; + self.store + .put_item(Ð1_CACHE_DB_KEY, ð1_chain.as_ssz_container())?; } Ok(()) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index ff47c7a2b..5dbabcdd8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -229,7 +229,7 @@ where .ok_or_else(|| "get_persisted_eth1_backend requires a store.".to_string())?; store - .get_item::<SszEth1>(&Hash256::from_slice(Ð1_CACHE_DB_KEY)) + .get_item::<SszEth1>(Ð1_CACHE_DB_KEY) .map_err(|e| format!("DB error whilst reading eth1 cache: {:?}", e)) } @@ -241,7 +241,7 @@ where .ok_or_else(|| "store_contains_beacon_chain requires a store.".to_string())?; Ok(store - .get_item::<PersistedBeaconChain>(&Hash256::from_slice(&BEACON_CHAIN_DB_KEY)) + .get_item::<PersistedBeaconChain>(&BEACON_CHAIN_DB_KEY) .map_err(|e| format!("DB error when reading persisted beacon chain: {:?}", e))? .is_some()) } @@ -272,7 +272,7 @@ where .ok_or_else(|| "resume_from_db requires a store.".to_string())?; let chain = store - .get_item::<PersistedBeaconChain>(&Hash256::from_slice(&BEACON_CHAIN_DB_KEY)) + .get_item::<PersistedBeaconChain>(&BEACON_CHAIN_DB_KEY) .map_err(|e| format!("DB error when reading persisted beacon chain: {:?}", e))? .ok_or_else(|| { "No persisted beacon chain found in store. Try purging the beacon chain database." @@ -280,7 +280,7 @@ where })?; let persisted_fork_choice = store - .get_item::<PersistedForkChoice>(&Hash256::from_slice(&FORK_CHOICE_DB_KEY)) + .get_item::<PersistedForkChoice>(&FORK_CHOICE_DB_KEY) .map_err(|e| format!("DB error when reading persisted fork choice: {:?}", e))? .ok_or_else(|| "No persisted fork choice present in database.".to_string())?; @@ -307,7 +307,7 @@ where self.op_pool = Some( store - .get_item::<PersistedOperationPool<TEthSpec>>(&Hash256::from_slice(&OP_POOL_DB_KEY)) + .get_item::<PersistedOperationPool<TEthSpec>>(&OP_POOL_DB_KEY) .map_err(|e| format!("DB error whilst reading persisted op pool: {:?}", e))? .map(PersistedOperationPool::into_operation_pool) .unwrap_or_else(OperationPool::new), diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 721eb4091..cd8b56478 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -357,11 +357,10 @@ fn roundtrip_operation_pool() { .persist_op_pool() .expect("should persist op pool"); - let key = Hash256::from_slice(&OP_POOL_DB_KEY); let restored_op_pool = harness .chain .store - .get_item::<PersistedOperationPool<MinimalEthSpec>>(&key) + .get_item::<PersistedOperationPool<MinimalEthSpec>>(&OP_POOL_DB_KEY) .expect("should read db") .expect("should find op pool") .into_operation_pool(); diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index 214932442..c11fcd448 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -3,15 +3,14 @@ use std::sync::Arc; use store::{DBColumn, Error as StoreError, HotColdDB, ItemStore, StoreItem}; use types::{EthSpec, Hash256}; -/// 32-byte key for accessing the `DhtEnrs`. -pub const DHT_DB_KEY: &str = "PERSISTEDDHTPERSISTEDDHTPERSISTE"; +/// 32-byte key for accessing the `DhtEnrs`. All zero because `DhtEnrs` has its own column. +pub const DHT_DB_KEY: Hash256 = Hash256::zero(); pub fn load_dht<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( store: Arc<HotColdDB<E, Hot, Cold>>, ) -> Vec<Enr> { // Load DHT from store - let key = Hash256::from_slice(&DHT_DB_KEY.as_bytes()); - match store.get_item(&key) { + match store.get_item(&DHT_DB_KEY) { Ok(Some(p)) => { let p: PersistedDht = p; p.enrs @@ -25,9 +24,7 @@ pub fn persist_dht<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( store: Arc<HotColdDB<E, Hot, Cold>>, enrs: Vec<Enr>, ) -> Result<(), store::Error> { - let key = Hash256::from_slice(&DHT_DB_KEY.as_bytes()); - store.put_item(&key, &PersistedDht { enrs })?; - Ok(()) + store.put_item(&DHT_DB_KEY, &PersistedDht { enrs }) } /// Wrapper around DHT for persistence to disk. @@ -61,7 +58,7 @@ mod tests { use std::str::FromStr; use store::config::StoreConfig; use store::{HotColdDB, MemoryStore}; - use types::{ChainSpec, Hash256, MinimalEthSpec}; + use types::{ChainSpec, MinimalEthSpec}; #[test] fn test_persisted_dht() { let log = NullLoggerBuilder.build().unwrap(); @@ -71,11 +68,10 @@ mod tests { MemoryStore<MinimalEthSpec>, > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal(), log).unwrap(); let enrs = vec![Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap()]; - let key = Hash256::from_slice(&DHT_DB_KEY.as_bytes()); store - .put_item(&key, &PersistedDht { enrs: enrs.clone() }) + .put_item(&DHT_DB_KEY, &PersistedDht { enrs: enrs.clone() }) .unwrap(); - let dht: PersistedDht = store.get_item(&key).unwrap().unwrap(); + let dht: PersistedDht = store.get_item(&DHT_DB_KEY).unwrap().unwrap(); assert_eq!(dht.enrs, enrs); } } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index fd838e033..2ee3fa417 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -267,7 +267,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("slots-per-restore-point") .value_name("SLOT_COUNT") .help("Specifies how often a freezer DB restore point should be stored. \ - DO NOT DECREASE AFTER INITIALIZATION. [default: 2048 (mainnet) or 64 (minimal)]") + Cannot be changed after initialization. \ + [default: 2048 (mainnet) or 64 (minimal)]") .takes_value(true) ) .arg( diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index bebddf8fa..91cf5ec1c 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -1,11 +1,14 @@ +use crate::{DBColumn, Error, StoreItem}; use serde_derive::{Deserialize, Serialize}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; use types::{EthSpec, MinimalEthSpec}; pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048; pub const DEFAULT_BLOCK_CACHE_SIZE: usize = 5; /// Database configuration parameters. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] pub struct StoreConfig { /// Number of slots to wait between storing restore points in the freezer database. pub slots_per_restore_point: u64, @@ -13,6 +16,11 @@ pub struct StoreConfig { pub block_cache_size: usize, } +#[derive(Debug, Clone)] +pub enum StoreConfigError { + MismatchedSlotsPerRestorePoint { config: u64, on_disk: u64 }, +} + impl Default for StoreConfig { fn default() -> Self { Self { @@ -22,3 +30,29 @@ impl Default for StoreConfig { } } } + +impl StoreConfig { + pub fn check_compatibility(&self, on_disk_config: &Self) -> Result<(), StoreConfigError> { + if self.slots_per_restore_point != on_disk_config.slots_per_restore_point { + return Err(StoreConfigError::MismatchedSlotsPerRestorePoint { + config: self.slots_per_restore_point, + on_disk: on_disk_config.slots_per_restore_point, + }); + } + Ok(()) + } +} + +impl StoreItem for StoreConfig { + fn db_column() -> DBColumn { + DBColumn::BeaconMeta + } + + fn as_store_bytes(&self) -> Vec<u8> { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> { + Ok(Self::from_ssz_bytes(bytes)?) + } +} diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 8e9237361..622cd2ac7 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -1,4 +1,5 @@ use crate::chunked_vector::ChunkError; +use crate::config::StoreConfigError; use crate::hot_cold_store::HotColdDBError; use ssz::DecodeError; use types::{BeaconStateError, Hash256, Slot}; @@ -17,6 +18,7 @@ pub enum Error { BlockNotFound(Hash256), NoContinuationData, SplitPointModified(Slot, Slot), + ConfigError(StoreConfigError), } impl From<DecodeError> for Error { @@ -49,6 +51,12 @@ impl From<DBError> for Error { } } +impl From<StoreConfigError> for Error { + fn from(e: StoreConfigError) -> Error { + Error::ConfigError(e) + } +} + #[derive(Debug)] pub struct DBError { pub message: String, diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 08e810866..55c403aa8 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -7,6 +7,9 @@ use crate::impls::beacon_state::{get_full_state, store_full_state}; use crate::iter::{ParentRootBlockIterator, StateRootsIterator}; use crate::leveldb_store::LevelDB; use crate::memory_store::MemoryStore; +use crate::metadata::{ + SchemaVersion, CONFIG_KEY, CURRENT_SCHEMA_VERSION, SCHEMA_VERSION_KEY, SPLIT_KEY, +}; use crate::metrics; use crate::{ get_key_for_col, DBColumn, Error, ItemStore, KeyValueStoreOp, PartialBeaconState, StoreItem, @@ -27,9 +30,6 @@ use std::path::Path; use std::sync::Arc; use types::*; -/// 32-byte key for accessing the `split` of the freezer DB. -pub const SPLIT_DB_KEY: &str = "FREEZERDBSPLITFREEZERDBSPLITFREE"; - /// Defines how blocks should be replayed on states. #[derive(PartialEq)] pub enum BlockReplay { @@ -46,6 +46,8 @@ pub enum BlockReplay { /// intermittent "restore point" states pre-finalization. #[derive(Debug)] pub struct HotColdDB<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> { + /// The schema version. Loaded from disk on initialization. + schema_version: SchemaVersion, /// The slot and state root at the point where the database is split between hot and cold. /// /// States with slots less than `split.slot` are in the cold DB, while states with slots @@ -70,6 +72,10 @@ pub struct HotColdDB<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> { #[derive(Debug, PartialEq)] pub enum HotColdDBError { + UnsupportedSchemaVersion { + software_version: SchemaVersion, + disk_version: SchemaVersion, + }, /// Recoverable error indicating that the database freeze point couldn't be updated /// due to the finalized block not lying on an epoch boundary (should be infrequent). FreezeSlotUnaligned(Slot), @@ -106,6 +112,7 @@ impl<E: EthSpec> HotColdDB<E, MemoryStore<E>, MemoryStore<E>> { Self::verify_slots_per_restore_point(config.slots_per_restore_point)?; let db = HotColdDB { + schema_version: CURRENT_SCHEMA_VERSION, split: RwLock::new(Split::default()), cold_db: MemoryStore::open(), hot_db: MemoryStore::open(), @@ -134,6 +141,7 @@ impl<E: EthSpec> HotColdDB<E, LevelDB<E>, LevelDB<E>> { Self::verify_slots_per_restore_point(config.slots_per_restore_point)?; let db = HotColdDB { + schema_version: CURRENT_SCHEMA_VERSION, split: RwLock::new(Split::default()), cold_db: LevelDB::open(cold_path)?, hot_db: LevelDB::open(hot_path)?, @@ -144,12 +152,33 @@ impl<E: EthSpec> HotColdDB<E, LevelDB<E>, LevelDB<E>> { _phantom: PhantomData, }; + // Ensure that the schema version of the on-disk database matches the software. + // In the future, this would be the spot to hook in auto-migration, etc. + if let Some(schema_version) = db.load_schema_version()? { + if schema_version != CURRENT_SCHEMA_VERSION { + return Err(HotColdDBError::UnsupportedSchemaVersion { + software_version: CURRENT_SCHEMA_VERSION, + disk_version: schema_version, + } + .into()); + } + } else { + db.store_schema_version(CURRENT_SCHEMA_VERSION)?; + } + + // Ensure that any on-disk config is compatible with the supplied config. + if let Some(disk_config) = db.load_config()? { + db.config.check_compatibility(&disk_config)?; + } + db.store_config()?; + // Load the previous split slot from the database (if any). This ensures we can // stop and restart correctly. if let Some(split) = db.load_split()? { info!( db.log, "Hot-Cold DB initialized"; + "version" => db.schema_version.0, "split_slot" => split.slot, "split_state" => format!("{:?}", split.state_root) ); @@ -744,11 +773,29 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> * self.config.slots_per_restore_point } + /// Load the database schema version from disk. + fn load_schema_version(&self) -> Result<Option<SchemaVersion>, Error> { + self.hot_db.get(&SCHEMA_VERSION_KEY) + } + + /// Store the database schema version. + fn store_schema_version(&self, schema_version: SchemaVersion) -> Result<(), Error> { + self.hot_db.put(&SCHEMA_VERSION_KEY, &schema_version) + } + + /// Load previously-stored config from disk. + fn load_config(&self) -> Result<Option<StoreConfig>, Error> { + self.hot_db.get(&CONFIG_KEY) + } + + /// Write the config to disk. + fn store_config(&self) -> Result<(), Error> { + self.hot_db.put(&CONFIG_KEY, &self.config) + } + /// Load the split point from disk. fn load_split(&self) -> Result<Option<Split>, Error> { - let key = Hash256::from_slice(SPLIT_DB_KEY.as_bytes()); - let split: Option<Split> = self.hot_db.get(&key)?; - Ok(split) + self.hot_db.get(&SPLIT_KEY) } /// Load the state root of a restore point. @@ -927,9 +974,7 @@ pub fn migrate_database<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( slot: frozen_head.slot, state_root: frozen_head_root, }; - store - .hot_db - .put_sync(&Hash256::from_slice(SPLIT_DB_KEY.as_bytes()), &split)?; + store.hot_db.put_sync(&SPLIT_KEY, &split)?; // Split point is now persisted in the hot database on disk. The in-memory split point // hasn't been modified elsewhere since we keep a write lock on it. It's safe to update diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 271870226..f249be1f8 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -19,6 +19,7 @@ pub mod hot_cold_store; mod impls; mod leveldb_store; mod memory_store; +mod metadata; mod metrics; mod partial_beacon_state; @@ -153,7 +154,7 @@ pub enum DBColumn { } impl Into<&'static str> for DBColumn { - /// Returns a `&str` that can be used for keying a key-value data base. + /// Returns a `&str` prefix to be added to keys before they hit the key-value database. fn into(self) -> &'static str { match self { DBColumn::BeaconMeta => "bma", diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs new file mode 100644 index 000000000..2d4733d63 --- /dev/null +++ b/beacon_node/store/src/metadata.rs @@ -0,0 +1,29 @@ +use crate::{DBColumn, Error, StoreItem}; +use ssz::{Decode, Encode}; +use types::Hash256; + +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); + +// All the keys that get stored under the `BeaconMeta` column. +// +// We use `repeat_byte` because it's a const fn. +pub const SCHEMA_VERSION_KEY: Hash256 = Hash256::repeat_byte(0); +pub const CONFIG_KEY: Hash256 = Hash256::repeat_byte(1); +pub const SPLIT_KEY: Hash256 = Hash256::repeat_byte(2); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct SchemaVersion(pub u64); + +impl StoreItem for SchemaVersion { + fn db_column() -> DBColumn { + DBColumn::BeaconMeta + } + + fn as_store_bytes(&self) -> Vec<u8> { + self.0.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> { + Ok(SchemaVersion(u64::from_ssz_bytes(bytes)?)) + } +} From 1d278aaa8345bca513ec58a66b4a5c2fbd2d3f74 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michael@sigmaprime.io> Date: Fri, 2 Oct 2020 01:42:27 +0000 Subject: [PATCH 06/32] Implement slashing protection interchange format (#1544) ## Issue Addressed Implements support for importing and exporting the slashing protection DB interchange format described here: https://hackmd.io/@sproul/Bk0Y0qdGD Also closes #1584 ## Proposed Changes * [x] Support for serializing and deserializing the format * [x] Support for importing and exporting Lighthouse's database * [x] CLI commands to invoke import and export * [x] Export to minimal format (required when a minimal format has been previously imported) * [x] Tests for export to minimal (utilising mixed importing and attestation signing?) * [x] Tests for import/export of complete format, and import of minimal format * [x] ~~Prevent attestations with sources less than our max source (Danny's suggestion). Required for the fake attestation that we put in for the minimal format to block attestations from source 0.~~ * [x] Add the concept of a "low watermark" for compatibility with the minimal format Bonus! * [x] A fix to a potentially nasty bug involving validators getting re-registered each time the validator client ran! Thankfully, the ordering of keys meant that the validator IDs used for attestations and blocks remained stable -- otherwise we could have had some slashings on our hands! :scream: * [x] Tests to confirm that this bug is indeed vanquished --- .gitignore | 2 +- Cargo.lock | 6 + account_manager/Cargo.toml | 1 + account_manager/src/validator/create.rs | 42 +- account_manager/src/validator/import.rs | 26 + account_manager/src/validator/mod.rs | 5 + .../src/validator/slashing_protection.rs | 137 ++++++ lighthouse/Cargo.toml | 1 + lighthouse/tests/account_manager.rs | 59 ++- .../slashing_protection/.gitignore | 2 + .../slashing_protection/Cargo.toml | 4 + validator_client/slashing_protection/Makefile | 28 ++ validator_client/slashing_protection/build.rs | 7 + .../src/bin/test_generator.rs | 128 +++++ .../slashing_protection/src/interchange.rs | 84 ++++ .../src/interchange_test.rs | 151 ++++++ .../slashing_protection/src/lib.rs | 10 +- .../src/registration_tests.rs | 32 ++ .../src/signed_attestation.rs | 12 + .../slashing_protection/src/signed_block.rs | 1 + .../src/slashing_database.rs | 455 +++++++++++++++--- .../slashing_protection/src/test_utils.rs | 36 +- .../slashing_protection/tests/interop.rs | 23 + validator_client/src/config.rs | 2 - validator_client/src/validator_store.rs | 6 +- 25 files changed, 1168 insertions(+), 92 deletions(-) create mode 100644 account_manager/src/validator/slashing_protection.rs create mode 100644 validator_client/slashing_protection/.gitignore create mode 100644 validator_client/slashing_protection/Makefile create mode 100644 validator_client/slashing_protection/build.rs create mode 100644 validator_client/slashing_protection/src/bin/test_generator.rs create mode 100644 validator_client/slashing_protection/src/interchange.rs create mode 100644 validator_client/slashing_protection/src/interchange_test.rs create mode 100644 validator_client/slashing_protection/src/registration_tests.rs create mode 100644 validator_client/slashing_protection/tests/interop.rs diff --git a/.gitignore b/.gitignore index 570bb6cdf..d6b4306ef 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ target/ flamegraph.svg perf.data* *.tar.gz -bin/ +/bin diff --git a/Cargo.lock b/Cargo.lock index a94d97af3..34cf85678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ dependencies = [ "libc", "rand 0.7.3", "rayon", + "slashing_protection", "slog", "slog-async", "slog-term", @@ -3057,6 +3058,7 @@ dependencies = [ "futures 0.3.5", "lighthouse_version", "logging", + "slashing_protection", "slog", "slog-async", "slog-term", @@ -4935,6 +4937,10 @@ dependencies = [ "r2d2_sqlite", "rayon", "rusqlite", + "serde", + "serde_derive", + "serde_json", + "serde_utils", "tempfile", "tree_hash", "types", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 7127a2ddf..1d571489d 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -32,3 +32,4 @@ validator_dir = { path = "../common/validator_dir" } tokio = { version = "0.2.21", features = ["full"] } eth2_keystore = { path = "../crypto/eth2_keystore" } account_utils = { path = "../common/account_utils" } +slashing_protection = { path = "../validator_client/slashing_protection" } diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 0d4566e46..9c503ecc6 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -10,6 +10,7 @@ use directory::{ }; use environment::Environment; use eth2_wallet_manager::WalletManager; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; @@ -178,6 +179,16 @@ pub fn cli_run<T: EthSpec>( .wallet_by_name(&wallet_name) .map_err(|e| format!("Unable to open wallet: {:?}", e))?; + let slashing_protection_path = validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = + SlashingDatabase::open_or_create(&slashing_protection_path).map_err(|e| { + format!( + "Unable to open or create slashing protection database at {}: {:?}", + slashing_protection_path.display(), + e + ) + })?; + for i in 0..n { let voting_password = random_password(); let withdrawal_password = random_password(); @@ -190,7 +201,22 @@ pub fn cli_run<T: EthSpec>( ) .map_err(|e| format!("Unable to create validator keys: {:?}", e))?; - let voting_pubkey = keystores.voting.pubkey().to_string(); + let voting_pubkey = keystores.voting.public_key().ok_or_else(|| { + format!( + "Keystore public key is invalid: {}", + keystores.voting.pubkey() + ) + })?; + + slashing_protection + .register_validator(&voting_pubkey) + .map_err(|e| { + format!( + "Error registering validator {}: {:?}", + voting_pubkey.to_hex_string(), + e + ) + })?; ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) @@ -200,7 +226,7 @@ pub fn cli_run<T: EthSpec>( .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; - println!("{}/{}\t0x{}", i + 1, n, voting_pubkey); + println!("{}/{}\t{}", i + 1, n, voting_pubkey.to_hex_string()); } Ok(()) @@ -208,14 +234,18 @@ pub fn cli_run<T: EthSpec>( /// Returns the number of validators that exist in the given `validator_dir`. /// -/// This function just assumes all files and directories, excluding the validator definitions YAML, -/// are validator directories, making it likely to return a higher number than accurate -/// but never a lower one. +/// This function just assumes all files and directories, excluding the validator definitions YAML +/// and slashing protection database are validator directories, making it likely to return a higher +/// number than accurate but never a lower one. fn existing_validator_count<P: AsRef<Path>>(validator_dir: P) -> Result<usize, String> { fs::read_dir(validator_dir.as_ref()) .map(|iter| { iter.filter_map(|e| e.ok()) - .filter(|e| e.file_name() != OsStr::new(validator_definitions::CONFIG_FILENAME)) + .filter(|e| { + e.file_name() != OsStr::new(validator_definitions::CONFIG_FILENAME) + && e.file_name() + != OsStr::new(slashing_protection::SLASHING_PROTECTION_FILENAME) + }) .count() }) .map_err(|e| format!("Unable to read {:?}: {}", validator_dir.as_ref(), e)) diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 1998709d2..8b4a216e8 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -9,6 +9,7 @@ use account_utils::{ ZeroizeString, }; use clap::{App, Arg, ArgMatches}; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::fs; use std::path::PathBuf; use std::thread::sleep; @@ -75,6 +76,16 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let mut defs = ValidatorDefinitions::open_or_create(&validator_dir) .map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?; + let slashing_protection_path = validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = + SlashingDatabase::open_or_create(&slashing_protection_path).map_err(|e| { + format!( + "Unable to open or create slashing protection database at {}: {:?}", + slashing_protection_path.display(), + e + ) + })?; + // Collect the paths for the keystores that should be imported. let keystore_paths = match (keystore, keystores_dir) { (Some(keystore), None) => vec![keystore], @@ -105,6 +116,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin // // - Obtain the keystore password, if the user desires. // - Copy the keystore into the `validator_dir`. + // - Register the voting key with the slashing protection database. // - Add the keystore to the validator definitions file. // // Skip keystores that already exist, but exit early if any operation fails. @@ -185,6 +197,20 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin fs::copy(&src_keystore, &dest_keystore) .map_err(|e| format!("Unable to copy keystore: {:?}", e))?; + // Register with slashing protection. + let voting_pubkey = keystore + .public_key() + .ok_or_else(|| format!("Keystore public key is invalid: {}", keystore.pubkey()))?; + slashing_protection + .register_validator(&voting_pubkey) + .map_err(|e| { + format!( + "Error registering validator {}: {:?}", + voting_pubkey.to_hex_string(), + e + ) + })?; + eprintln!("Successfully imported keystore."); num_imported_keystores += 1; diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 4c650dad0..99d8da01b 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -3,6 +3,7 @@ pub mod deposit; pub mod import; pub mod list; pub mod recover; +pub mod slashing_protection; use crate::VALIDATOR_DIR_FLAG; use clap::{App, Arg, ArgMatches}; @@ -33,6 +34,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(import::cli_app()) .subcommand(list::cli_app()) .subcommand(recover::cli_app()) + .subcommand(slashing_protection::cli_app()) } pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<(), String> { @@ -50,6 +52,9 @@ pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result< (import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir), (list::CMD, Some(_)) => list::cli_run(validator_base_dir), (recover::CMD, Some(matches)) => recover::cli_run(matches, validator_base_dir), + (slashing_protection::CMD, Some(matches)) => { + slashing_protection::cli_run(matches, env, validator_base_dir) + } (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs new file mode 100644 index 000000000..53a7edd51 --- /dev/null +++ b/account_manager/src/validator/slashing_protection.rs @@ -0,0 +1,137 @@ +use clap::{App, Arg, ArgMatches}; +use environment::Environment; +use slashing_protection::{ + interchange::Interchange, SlashingDatabase, SLASHING_PROTECTION_FILENAME, +}; +use std::fs::File; +use std::path::PathBuf; +use types::EthSpec; + +pub const CMD: &str = "slashing-protection"; +pub const IMPORT_CMD: &str = "import"; +pub const EXPORT_CMD: &str = "export"; + +pub const IMPORT_FILE_ARG: &str = "IMPORT-FILE"; +pub const EXPORT_FILE_ARG: &str = "EXPORT-FILE"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about("Import or export slashing protection data to or from another client") + .subcommand( + App::new(IMPORT_CMD) + .about("Import an interchange file") + .arg( + Arg::with_name(IMPORT_FILE_ARG) + .takes_value(true) + .value_name("FILE") + .help("The slashing protection interchange file to import (.json)"), + ), + ) + .subcommand( + App::new(EXPORT_CMD) + .about("Export an interchange file") + .arg( + Arg::with_name(EXPORT_FILE_ARG) + .takes_value(true) + .value_name("FILE") + .help("The filename to export the interchange file to"), + ), + ) +} + +pub fn cli_run<T: EthSpec>( + matches: &ArgMatches<'_>, + env: Environment<T>, + validator_base_dir: PathBuf, +) -> Result<(), String> { + let slashing_protection_db_path = validator_base_dir.join(SLASHING_PROTECTION_FILENAME); + + let genesis_validators_root = env + .testnet + .and_then(|testnet_config| { + Some( + testnet_config + .genesis_state + .as_ref()? + .genesis_validators_root, + ) + }) + .ok_or_else(|| { + "Unable to get genesis validators root from testnet config, has genesis occurred?" + })?; + + match matches.subcommand() { + (IMPORT_CMD, Some(matches)) => { + let import_filename: PathBuf = clap_utils::parse_required(&matches, IMPORT_FILE_ARG)?; + let import_file = File::open(&import_filename).map_err(|e| { + format!( + "Unable to open import file at {}: {:?}", + import_filename.display(), + e + ) + })?; + + let interchange = Interchange::from_json_reader(&import_file) + .map_err(|e| format!("Error parsing file for import: {:?}", e))?; + + let slashing_protection_database = + SlashingDatabase::open_or_create(&slashing_protection_db_path).map_err(|e| { + format!( + "Unable to open database at {}: {:?}", + slashing_protection_db_path.display(), + e + ) + })?; + + slashing_protection_database + .import_interchange_info(&interchange, genesis_validators_root) + .map_err(|e| { + format!( + "Error during import, no data imported: {:?}\n\ + IT IS NOT SAFE TO START VALIDATING", + e + ) + })?; + + eprintln!("Import completed successfully"); + + Ok(()) + } + (EXPORT_CMD, Some(matches)) => { + let export_filename: PathBuf = clap_utils::parse_required(&matches, EXPORT_FILE_ARG)?; + + if !slashing_protection_db_path.exists() { + return Err(format!( + "No slashing protection database exists at: {}", + slashing_protection_db_path.display() + )); + } + + let slashing_protection_database = SlashingDatabase::open(&slashing_protection_db_path) + .map_err(|e| { + format!( + "Unable to open database at {}: {:?}", + slashing_protection_db_path.display(), + e + ) + })?; + + let interchange = slashing_protection_database + .export_interchange_info(genesis_validators_root) + .map_err(|e| format!("Error during export: {:?}", e))?; + + let output_file = File::create(export_filename) + .map_err(|e| format!("Error creating output file: {:?}", e))?; + + interchange + .write_to(&output_file) + .map_err(|e| format!("Error writing output file: {:?}", e))?; + + eprintln!("Export completed successfully"); + + Ok(()) + } + ("", _) => Err("No subcommand provided, see --help for options".to_string()), + (command, _) => Err(format!("No such subcommand `{}`", command)), + } +} diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 1daf5f97c..b7468d6a4 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -38,3 +38,4 @@ account_utils = { path = "../common/account_utils" } [dev-dependencies] tempfile = "3.1.0" validator_dir = { path = "../common/validator_dir" } +slashing_protection = { path = "../validator_client/slashing_protection" } diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 30f885b4e..3c963f5b1 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -18,6 +18,7 @@ use account_utils::{ validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions}, ZeroizeString, }; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::env; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Write}; @@ -25,7 +26,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use std::str::from_utf8; use tempfile::{tempdir, TempDir}; -use types::Keypair; +use types::{Keypair, PublicKey}; use validator_dir::ValidatorDir; // TODO: create tests for the `lighthouse account validator deposit` command. This involves getting @@ -69,6 +70,23 @@ fn dir_child_count<P: AsRef<Path>>(dir: P) -> usize { fs::read_dir(dir).expect("should read dir").count() } +/// Returns the number of 0x-prefixed children in a directory +/// i.e. validators in the validators dir. +fn dir_validator_count<P: AsRef<Path>>(dir: P) -> usize { + fs::read_dir(dir) + .unwrap() + .filter(|c| { + c.as_ref() + .unwrap() + .path() + .file_name() + .unwrap() + .to_string_lossy() + .starts_with("0x") + }) + .count() +} + /// Uses `lighthouse account wallet list` to list all wallets. fn list_wallets<P: AsRef<Path>>(base_dir: P) -> Vec<String> { let output = output_result( @@ -328,19 +346,30 @@ fn validator_create() { let wallet = TestWallet::new(base_dir.path(), "wally"); wallet.create_expect_success(); - assert_eq!(dir_child_count(validator_dir.path()), 0); + assert_eq!(dir_validator_count(validator_dir.path()), 0); let validator = TestValidator::new(validator_dir.path(), secrets_dir.path(), wallet); // Create a validator _without_ storing the withdraw key. - validator.create_expect_success(COUNT_FLAG, 1, false); + let created_validators = validator.create_expect_success(COUNT_FLAG, 1, false); - assert_eq!(dir_child_count(validator_dir.path()), 1); + // Validator should be registered with slashing protection. + check_slashing_protection( + &validator_dir, + created_validators + .iter() + .map(|v| v.voting_keypair(&secrets_dir).unwrap().pk), + ); + drop(created_validators); + + // Number of dir entries should be #validators + 1 for the slashing protection DB + assert_eq!(dir_validator_count(validator_dir.path()), 1); + assert_eq!(dir_child_count(validator_dir.path()), 2); // Create a validator storing the withdraw key. validator.create_expect_success(COUNT_FLAG, 1, true); - assert_eq!(dir_child_count(validator_dir.path()), 2); + assert_eq!(dir_validator_count(validator_dir.path()), 2); // Use the at-most flag with less validators then are in the directory. assert_eq!( @@ -348,7 +377,7 @@ fn validator_create() { 0 ); - assert_eq!(dir_child_count(validator_dir.path()), 2); + assert_eq!(dir_validator_count(validator_dir.path()), 2); // Use the at-most flag with the same number of validators that are in the directory. assert_eq!( @@ -356,7 +385,7 @@ fn validator_create() { 0 ); - assert_eq!(dir_child_count(validator_dir.path()), 2); + assert_eq!(dir_validator_count(validator_dir.path()), 2); // Use the at-most flag with two more number of validators than are in the directory. assert_eq!( @@ -364,7 +393,7 @@ fn validator_create() { 2 ); - assert_eq!(dir_child_count(validator_dir.path()), 4); + assert_eq!(dir_validator_count(validator_dir.path()), 4); // Create multiple validators with the count flag. assert_eq!( @@ -372,7 +401,7 @@ fn validator_create() { 2 ); - assert_eq!(dir_child_count(validator_dir.path()), 6); + assert_eq!(dir_validator_count(validator_dir.path()), 6); } #[test] @@ -445,6 +474,9 @@ fn validator_import_launchpad() { "not-keystore should not be present in dst dir" ); + // Validator should be registered with slashing protection. + check_slashing_protection(&dst_dir, std::iter::once(keystore.public_key().unwrap())); + let defs = ValidatorDefinitions::open(&dst_dir).unwrap(); let expected_def = ValidatorDefinition { @@ -462,3 +494,12 @@ fn validator_import_launchpad() { "validator defs file should be accurate" ); } + +/// Check that all of the given pubkeys have been registered with slashing protection. +fn check_slashing_protection(validator_dir: &TempDir, pubkeys: impl Iterator<Item = PublicKey>) { + let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); + let slashing_db = SlashingDatabase::open(&slashing_db_path).unwrap(); + for validator_pk in pubkeys { + slashing_db.get_validator_id(&validator_pk).unwrap(); + } +} diff --git a/validator_client/slashing_protection/.gitignore b/validator_client/slashing_protection/.gitignore new file mode 100644 index 000000000..10366122b --- /dev/null +++ b/validator_client/slashing_protection/.gitignore @@ -0,0 +1,2 @@ +interchange-tests +generated-tests diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 8966cb278..6145826fd 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -12,6 +12,10 @@ rusqlite = { version = "0.23.1", features = ["bundled"] } r2d2 = "0.8.8" r2d2_sqlite = "0.16.0" parking_lot = "0.11.0" +serde = "1.0.110" +serde_derive = "1.0.110" +serde_json = "1.0.52" +serde_utils = { path = "../../consensus/serde_utils" } [dev-dependencies] rayon = "1.3.0" diff --git a/validator_client/slashing_protection/Makefile b/validator_client/slashing_protection/Makefile new file mode 100644 index 000000000..5abb6c0a4 --- /dev/null +++ b/validator_client/slashing_protection/Makefile @@ -0,0 +1,28 @@ +TESTS_TAG := ac393b815b356c95569c028c215232b512df583d +GENERATE_DIR := generated-tests +OUTPUT_DIR := interchange-tests +TARBALL := $(OUTPUT_DIR)-$(TESTS_TAG).tar.gz +ARCHIVE_URL := https://github.com/eth2-clients/slashing-protection-interchange-tests/tarball/$(TESTS_TAG) + +$(OUTPUT_DIR): $(TARBALL) + rm -rf $@ + mkdir $@ + tar --strip-components=1 -xzf $^ -C $@ + +$(TARBALL): + wget $(ARCHIVE_URL) -O $@ + +clean-test-files: + rm -rf $(OUTPUT_DIR) + +clean-archives: + rm -f $(TARBALL) + +generate: + rm -rf $(GENERATE_DIR) + cargo run --release --bin test_generator -- $(GENERATE_DIR) + +clean: clean-test-files clean-archives + +.PHONY: clean clean-archives clean-test-files generate + diff --git a/validator_client/slashing_protection/build.rs b/validator_client/slashing_protection/build.rs new file mode 100644 index 000000000..03abb88b4 --- /dev/null +++ b/validator_client/slashing_protection/build.rs @@ -0,0 +1,7 @@ +fn main() { + let exit_status = std::process::Command::new("make") + .current_dir(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .status() + .unwrap(); + assert!(exit_status.success()); +} diff --git a/validator_client/slashing_protection/src/bin/test_generator.rs b/validator_client/slashing_protection/src/bin/test_generator.rs new file mode 100644 index 000000000..3522adf3f --- /dev/null +++ b/validator_client/slashing_protection/src/bin/test_generator.rs @@ -0,0 +1,128 @@ +use slashing_protection::interchange::{ + CompleteInterchangeData, Interchange, InterchangeFormat, InterchangeMetadata, + SignedAttestation, SignedBlock, +}; +use slashing_protection::interchange_test::TestCase; +use slashing_protection::test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}; +use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION; +use std::fs::{self, File}; +use std::path::Path; +use types::{Epoch, Hash256, Slot}; + +fn metadata(genesis_validators_root: Hash256) -> InterchangeMetadata { + InterchangeMetadata { + interchange_format: InterchangeFormat::Complete, + interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION, + genesis_validators_root, + } +} + +#[allow(clippy::type_complexity)] +fn interchange(data: Vec<(usize, Vec<u64>, Vec<(u64, u64)>)>) -> Interchange { + let data = data + .into_iter() + .map(|(pk, blocks, attestations)| CompleteInterchangeData { + pubkey: pubkey(pk), + signed_blocks: blocks + .into_iter() + .map(|slot| SignedBlock { + slot: Slot::new(slot), + signing_root: None, + }) + .collect(), + signed_attestations: attestations + .into_iter() + .map(|(source, target)| SignedAttestation { + source_epoch: Epoch::new(source), + target_epoch: Epoch::new(target), + signing_root: None, + }) + .collect(), + }) + .collect(); + Interchange { + metadata: metadata(DEFAULT_GENESIS_VALIDATORS_ROOT), + data, + } +} + +fn main() { + let single_validator_blocks = + vec![(0, 32, false), (0, 33, true), (0, 31, false), (0, 1, false)]; + let single_validator_attestations = vec![ + (0, 3, 4, false), + (0, 14, 19, false), + (0, 15, 20, false), + (0, 16, 20, false), + (0, 15, 21, true), + ]; + + let tests = vec![ + TestCase::new( + "single_validator_import_only", + interchange(vec![(0, vec![22], vec![(0, 2)])]), + ), + TestCase::new( + "single_validator_single_block", + interchange(vec![(0, vec![32], vec![])]), + ) + .with_blocks(single_validator_blocks.clone()), + TestCase::new( + "single_validator_single_attestation", + interchange(vec![(0, vec![], vec![(15, 20)])]), + ) + .with_attestations(single_validator_attestations.clone()), + TestCase::new( + "single_validator_single_block_and_attestation", + interchange(vec![(0, vec![32], vec![(15, 20)])]), + ) + .with_blocks(single_validator_blocks) + .with_attestations(single_validator_attestations), + TestCase::new( + "single_validator_genesis_attestation", + interchange(vec![(0, vec![], vec![(0, 0)])]), + ) + .with_attestations(vec![(0, 0, 0, false)]), + TestCase::new( + "single_validator_multiple_blocks_and_attestations", + interchange(vec![( + 0, + vec![2, 3, 10, 1200], + vec![(10, 11), (12, 13), (20, 24)], + )]), + ) + .with_blocks(vec![ + (0, 1, false), + (0, 2, false), + (0, 3, false), + (0, 10, false), + (0, 1200, false), + (0, 4, true), + (0, 256, true), + (0, 1201, true), + ]) + .with_attestations(vec![ + (0, 9, 10, false), + (0, 12, 13, false), + (0, 11, 14, false), + (0, 21, 22, false), + (0, 10, 24, false), + (0, 11, 12, true), + (0, 20, 25, true), + ]), + TestCase::new("wrong_genesis_validators_root", interchange(vec![])) + .gvr(Hash256::from_low_u64_be(1)) + .should_fail(), + ]; + // TODO: multi-validator test + + let args = std::env::args().collect::<Vec<_>>(); + let output_dir = Path::new(&args[1]); + fs::create_dir_all(output_dir).unwrap(); + + for test in tests { + test.run(); + let f = File::create(output_dir.join(format!("{}.json", test.name))).unwrap(); + serde_json::to_writer(f, &test).unwrap(); + } +} diff --git a/validator_client/slashing_protection/src/interchange.rs b/validator_client/slashing_protection/src/interchange.rs new file mode 100644 index 000000000..71f678c59 --- /dev/null +++ b/validator_client/slashing_protection/src/interchange.rs @@ -0,0 +1,84 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::iter::FromIterator; +use types::{Epoch, Hash256, PublicKey, Slot}; + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum InterchangeFormat { + Complete, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct InterchangeMetadata { + pub interchange_format: InterchangeFormat, + #[serde(with = "serde_utils::quoted_u64::require_quotes")] + pub interchange_format_version: u64, + pub genesis_validators_root: Hash256, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct CompleteInterchangeData { + pub pubkey: PublicKey, + pub signed_blocks: Vec<SignedBlock>, + pub signed_attestations: Vec<SignedAttestation>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SignedBlock { + #[serde(with = "serde_utils::quoted_u64::require_quotes")] + pub slot: Slot, + #[serde(skip_serializing_if = "Option::is_none")] + pub signing_root: Option<Hash256>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SignedAttestation { + #[serde(with = "serde_utils::quoted_u64::require_quotes")] + pub source_epoch: Epoch, + #[serde(with = "serde_utils::quoted_u64::require_quotes")] + pub target_epoch: Epoch, + #[serde(skip_serializing_if = "Option::is_none")] + pub signing_root: Option<Hash256>, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct Interchange { + pub metadata: InterchangeMetadata, + pub data: Vec<CompleteInterchangeData>, +} + +impl Interchange { + pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> { + serde_json::from_str(json) + } + + pub fn from_json_reader(reader: impl std::io::Read) -> Result<Self, serde_json::Error> { + serde_json::from_reader(reader) + } + + pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> { + serde_json::to_writer(writer, self) + } + + /// Do these two `Interchange`s contain the same data (ignoring ordering)? + pub fn equiv(&self, other: &Self) -> bool { + let self_set = HashSet::<_>::from_iter(self.data.iter()); + let other_set = HashSet::<_>::from_iter(other.data.iter()); + self.metadata == other.metadata && self_set == other_set + } + + /// The number of entries in `data`. + pub fn len(&self) -> usize { + self.data.len() + } + + /// Is the `data` part of the interchange completely empty? + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs new file mode 100644 index 000000000..cbb8c54a9 --- /dev/null +++ b/validator_client/slashing_protection/src/interchange_test.rs @@ -0,0 +1,151 @@ +use crate::{ + interchange::Interchange, + test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}, + SlashingDatabase, +}; +use serde_derive::{Deserialize, Serialize}; +use tempfile::tempdir; +use types::{Epoch, Hash256, PublicKey, Slot}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestCase { + pub name: String, + pub should_succeed: bool, + pub genesis_validators_root: Hash256, + pub interchange: Interchange, + pub blocks: Vec<TestBlock>, + pub attestations: Vec<TestAttestation>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestBlock { + pub pubkey: PublicKey, + pub slot: Slot, + pub should_succeed: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestAttestation { + pub pubkey: PublicKey, + pub source_epoch: Epoch, + pub target_epoch: Epoch, + pub should_succeed: bool, +} + +impl TestCase { + pub fn new(name: &str, interchange: Interchange) -> Self { + TestCase { + name: name.into(), + should_succeed: true, + genesis_validators_root: DEFAULT_GENESIS_VALIDATORS_ROOT, + interchange, + blocks: vec![], + attestations: vec![], + } + } + + pub fn gvr(mut self, genesis_validators_root: Hash256) -> Self { + self.genesis_validators_root = genesis_validators_root; + self + } + + pub fn should_fail(mut self) -> Self { + self.should_succeed = false; + self + } + + pub fn with_blocks(mut self, blocks: impl IntoIterator<Item = (usize, u64, bool)>) -> Self { + self.blocks.extend( + blocks + .into_iter() + .map(|(pk, slot, should_succeed)| TestBlock { + pubkey: pubkey(pk), + slot: Slot::new(slot), + should_succeed, + }), + ); + self + } + + pub fn with_attestations( + mut self, + attestations: impl IntoIterator<Item = (usize, u64, u64, bool)>, + ) -> Self { + self.attestations.extend(attestations.into_iter().map( + |(pk, source, target, should_succeed)| TestAttestation { + pubkey: pubkey(pk), + source_epoch: Epoch::new(source), + target_epoch: Epoch::new(target), + should_succeed, + }, + )); + self + } + + pub fn run(&self) { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + match slashing_db.import_interchange_info(&self.interchange, self.genesis_validators_root) { + Ok(()) if !self.should_succeed => { + panic!( + "test `{}` succeeded on import when it should have failed", + self.name + ); + } + Err(e) if self.should_succeed => { + panic!( + "test `{}` failed on import when it should have succeeded, error: {:?}", + self.name, e + ); + } + _ => (), + } + + for (i, block) in self.blocks.iter().enumerate() { + match slashing_db.check_and_insert_block_signing_root( + &block.pubkey, + block.slot, + Hash256::random(), + ) { + Ok(safe) if !block.should_succeed => { + panic!( + "block {} from `{}` succeeded when it should have failed: {:?}", + i, self.name, safe + ); + } + Err(e) if block.should_succeed => { + panic!( + "block {} from `{}` failed when it should have succeeded: {:?}", + i, self.name, e + ); + } + _ => (), + } + } + + for (i, att) in self.attestations.iter().enumerate() { + match slashing_db.check_and_insert_attestation_signing_root( + &att.pubkey, + att.source_epoch, + att.target_epoch, + Hash256::random(), + ) { + Ok(safe) if !att.should_succeed => { + panic!( + "attestation {} from `{}` succeeded when it should have failed: {:?}", + i, self.name, safe + ); + } + Err(e) if att.should_succeed => { + panic!( + "attestation {} from `{}` failed when it should have succeeded: {:?}", + i, self.name, e + ); + } + _ => (), + } + } + } +} diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index 384523495..a576743aa 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -1,19 +1,25 @@ mod attestation_tests; mod block_tests; +pub mod interchange; +pub mod interchange_test; mod parallel_tests; +mod registration_tests; mod signed_attestation; mod signed_block; mod slashing_database; -mod test_utils; +pub mod test_utils; pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation}; pub use crate::signed_block::{InvalidBlock, SignedBlock}; -pub use crate::slashing_database::SlashingDatabase; +pub use crate::slashing_database::{SlashingDatabase, SUPPORTED_INTERCHANGE_FORMAT_VERSION}; use rusqlite::Error as SQLError; use std::io::{Error as IOError, ErrorKind}; use std::string::ToString; use types::{Hash256, PublicKey}; +/// The filename within the `validators` directory that contains the slashing protection DB. +pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; + /// The attestation or block is not safe to sign. /// /// This could be because it's slashable, or because an error occurred. diff --git a/validator_client/slashing_protection/src/registration_tests.rs b/validator_client/slashing_protection/src/registration_tests.rs new file mode 100644 index 000000000..40a3d6ee7 --- /dev/null +++ b/validator_client/slashing_protection/src/registration_tests.rs @@ -0,0 +1,32 @@ +#![cfg(test)] + +use crate::test_utils::*; +use crate::*; +use tempfile::tempdir; + +#[test] +fn double_register_validators() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let num_validators = 100u32; + let pubkeys = (0..num_validators as usize).map(pubkey).collect::<Vec<_>>(); + + let get_validator_ids = || { + pubkeys + .iter() + .map(|pk| slashing_db.get_validator_id(pk).unwrap()) + .collect::<Vec<_>>() + }; + + assert_eq!(slashing_db.num_validator_rows().unwrap(), 0); + + slashing_db.register_validators(pubkeys.iter()).unwrap(); + assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); + let validator_ids = get_validator_ids(); + + slashing_db.register_validators(pubkeys.iter()).unwrap(); + assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); + assert_eq!(validator_ids, get_validator_ids()); +} diff --git a/validator_client/slashing_protection/src/signed_attestation.rs b/validator_client/slashing_protection/src/signed_attestation.rs index 3ab586e4e..1c8020614 100644 --- a/validator_client/slashing_protection/src/signed_attestation.rs +++ b/validator_client/slashing_protection/src/signed_attestation.rs @@ -20,6 +20,18 @@ pub enum InvalidAttestation { PrevSurroundsNew { prev: SignedAttestation }, /// The attestation is invalid because its source epoch is greater than its target epoch. SourceExceedsTarget, + /// The attestation is invalid because its source epoch is less than the lower bound on source + /// epochs for this validator. + SourceLessThanLowerBound { + source_epoch: Epoch, + bound_epoch: Epoch, + }, + /// The attestation is invalid because its target epoch is less than or equal to the lower + /// bound on target epochs for this validator. + TargetLessThanOrEqLowerBound { + target_epoch: Epoch, + bound_epoch: Epoch, + }, } impl SignedAttestation { diff --git a/validator_client/slashing_protection/src/signed_block.rs b/validator_client/slashing_protection/src/signed_block.rs index f299871a6..b31628f43 100644 --- a/validator_client/slashing_protection/src/signed_block.rs +++ b/validator_client/slashing_protection/src/signed_block.rs @@ -12,6 +12,7 @@ pub struct SignedBlock { #[derive(PartialEq, Debug)] pub enum InvalidBlock { DoubleBlockProposal(SignedBlock), + SlotViolatesLowerBound { block_slot: Slot, bound_slot: Slot }, } impl SignedBlock { diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index cd2413efd..df0b38ecf 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -1,12 +1,16 @@ +use crate::interchange::{ + CompleteInterchangeData, Interchange, InterchangeFormat, InterchangeMetadata, + SignedAttestation as InterchangeAttestation, SignedBlock as InterchangeBlock, +}; use crate::signed_attestation::InvalidAttestation; use crate::signed_block::InvalidBlock; -use crate::{NotSafe, Safe, SignedAttestation, SignedBlock}; +use crate::{hash256_from_row, NotSafe, Safe, SignedAttestation, SignedBlock}; use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior}; use std::fs::{File, OpenOptions}; use std::path::Path; use std::time::Duration; -use types::{AttestationData, BeaconBlockHeader, Hash256, PublicKey, SignedRoot}; +use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, PublicKey, SignedRoot, Slot}; type Pool = r2d2::Pool<SqliteConnectionManager>; @@ -20,6 +24,9 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); #[cfg(test)] pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100); +/// Supported version of the interchange format. +pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 4; + #[derive(Debug, Clone)] pub struct SlashingDatabase { conn_pool: Pool, @@ -52,7 +59,7 @@ impl SlashingDatabase { conn.execute( "CREATE TABLE validators ( id INTEGER PRIMARY KEY, - public_key BLOB NOT NULL + public_key BLOB NOT NULL UNIQUE )", params![], )?; @@ -144,15 +151,25 @@ impl SlashingDatabase { ) -> Result<(), NotSafe> { let mut conn = self.conn_pool.get()?; let txn = conn.transaction()?; - { - let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?; + self.register_validators_in_txn(public_keys, &txn)?; + txn.commit()?; + Ok(()) + } - for pubkey in public_keys { + /// Register multiple validators inside the given transaction. + /// + /// The caller must commit the transaction for the changes to be persisted. + pub fn register_validators_in_txn<'a>( + &self, + public_keys: impl Iterator<Item = &'a PublicKey>, + txn: &Transaction, + ) -> Result<(), NotSafe> { + let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?; + for pubkey in public_keys { + if self.get_validator_id_opt(&txn, pubkey)?.is_none() { stmt.execute(&[pubkey.to_hex_string()])?; } } - txn.commit()?; - Ok(()) } @@ -160,14 +177,34 @@ impl SlashingDatabase { /// /// This is NOT the same as a validator index, and depends on the ordering that validators /// are registered with the slashing protection database (and may vary between machines). - fn get_validator_id(txn: &Transaction, public_key: &PublicKey) -> Result<i64, NotSafe> { - txn.query_row( - "SELECT id FROM validators WHERE public_key = ?1", - params![&public_key.to_hex_string()], - |row| row.get(0), - ) - .optional()? - .ok_or_else(|| NotSafe::UnregisteredValidator(public_key.clone())) + pub fn get_validator_id(&self, public_key: &PublicKey) -> Result<i64, NotSafe> { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + self.get_validator_id_in_txn(&txn, public_key) + } + + fn get_validator_id_in_txn( + &self, + txn: &Transaction, + public_key: &PublicKey, + ) -> Result<i64, NotSafe> { + self.get_validator_id_opt(txn, public_key)? + .ok_or_else(|| NotSafe::UnregisteredValidator(public_key.clone())) + } + + /// Optional version of `get_validator_id`. + fn get_validator_id_opt( + &self, + txn: &Transaction, + public_key: &PublicKey, + ) -> Result<Option<i64>, NotSafe> { + Ok(txn + .query_row( + "SELECT id FROM validators WHERE public_key = ?1", + params![&public_key.to_hex_string()], + |row| row.get(0), + ) + .optional()?) } /// Check a block proposal from `validator_pubkey` for slash safety. @@ -175,10 +212,10 @@ impl SlashingDatabase { &self, txn: &Transaction, validator_pubkey: &PublicKey, - block_header: &BeaconBlockHeader, - domain: Hash256, + slot: Slot, + signing_root: Hash256, ) -> Result<Safe, NotSafe> { - let validator_id = Self::get_validator_id(txn, validator_pubkey)?; + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; let existing_block = txn .prepare( @@ -186,25 +223,37 @@ impl SlashingDatabase { FROM signed_blocks WHERE validator_id = ?1 AND slot = ?2", )? - .query_row( - params![validator_id, block_header.slot], - SignedBlock::from_row, - ) + .query_row(params![validator_id, slot], SignedBlock::from_row) .optional()?; if let Some(existing_block) = existing_block { - if existing_block.signing_root == block_header.signing_root(domain) { + if existing_block.signing_root == signing_root { // Same slot and same hash -> we're re-broadcasting a previously signed block - Ok(Safe::SameData) + return Ok(Safe::SameData); } else { // Same epoch but not the same hash -> it's a DoubleBlockProposal - Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal( + return Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal( existing_block, - ))) + ))); } - } else { - Ok(Safe::Valid) } + + let min_slot = txn + .prepare("SELECT MIN(slot) FROM signed_blocks WHERE validator_id = ?1")? + .query_row(params![validator_id], |row| row.get(0))?; + + if let Some(min_slot) = min_slot { + if slot <= min_slot { + return Err(NotSafe::InvalidBlock( + InvalidBlock::SlotViolatesLowerBound { + block_slot: slot, + bound_slot: min_slot, + }, + )); + } + } + + Ok(Safe::Valid) } /// Check an attestation from `validator_pubkey` for slash safety. @@ -212,12 +261,10 @@ impl SlashingDatabase { &self, txn: &Transaction, validator_pubkey: &PublicKey, - attestation: &AttestationData, - domain: Hash256, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: Hash256, ) -> Result<Safe, NotSafe> { - let att_source_epoch = attestation.source.epoch; - let att_target_epoch = attestation.target.epoch; - // Although it's not required to avoid slashing, we disallow attestations // which are obviously invalid by virtue of their source epoch exceeding their target. if att_source_epoch > att_target_epoch { @@ -226,10 +273,10 @@ impl SlashingDatabase { )); } - let validator_id = Self::get_validator_id(txn, validator_pubkey)?; + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; - // 1. Check for a double vote. Namely, an existing attestation with the same target epoch, - // and a different signing root. + // Check for a double vote. Namely, an existing attestation with the same target epoch, + // and a different signing root. let same_target_att = txn .prepare( "SELECT source_epoch, target_epoch, signing_root @@ -245,7 +292,7 @@ impl SlashingDatabase { if let Some(existing_attestation) = same_target_att { // If the new attestation is identical to the existing attestation, then we already // know that it is safe, and can return immediately. - if existing_attestation.signing_root == attestation.signing_root(domain) { + if existing_attestation.signing_root == att_signing_root { return Ok(Safe::SameData); // Otherwise if the hashes are different, this is a double vote. } else { @@ -255,7 +302,7 @@ impl SlashingDatabase { } } - // 2. Check that no previous vote is surrounding `attestation`. + // Check that no previous vote is surrounding `attestation`. // If there is a surrounding attestation, we only return the most recent one. let surrounding_attestation = txn .prepare( @@ -277,7 +324,7 @@ impl SlashingDatabase { )); } - // 3. Check that no previous vote is surrounded by `attestation`. + // Check that no previous vote is surrounded by `attestation`. // If there is a surrounded attestation, we only return the most recent one. let surrounded_attestation = txn .prepare( @@ -299,6 +346,39 @@ impl SlashingDatabase { )); } + // Check lower bounds: ensure that source is greater than or equal to min source, + // and target is greater than min target. This allows pruning, and compatibility + // with the interchange format. + let min_source = txn + .prepare("SELECT MIN(source_epoch) FROM signed_attestations WHERE validator_id = ?1")? + .query_row(params![validator_id], |row| row.get(0))?; + + if let Some(min_source) = min_source { + if att_source_epoch < min_source { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::SourceLessThanLowerBound { + source_epoch: att_source_epoch, + bound_epoch: min_source, + }, + )); + } + } + + let min_target = txn + .prepare("SELECT MIN(target_epoch) FROM signed_attestations WHERE validator_id = ?1")? + .query_row(params![validator_id], |row| row.get(0))?; + + if let Some(min_target) = min_target { + if att_target_epoch <= min_target { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::TargetLessThanOrEqLowerBound { + target_epoch: att_target_epoch, + bound_epoch: min_target, + }, + )); + } + } + // Everything has been checked, return Valid Ok(Safe::Valid) } @@ -311,19 +391,15 @@ impl SlashingDatabase { &self, txn: &Transaction, validator_pubkey: &PublicKey, - block_header: &BeaconBlockHeader, - domain: Hash256, + slot: Slot, + signing_root: Hash256, ) -> Result<(), NotSafe> { - let validator_id = Self::get_validator_id(txn, validator_pubkey)?; + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; txn.execute( "INSERT INTO signed_blocks (validator_id, slot, signing_root) VALUES (?1, ?2, ?3)", - params![ - validator_id, - block_header.slot, - block_header.signing_root(domain).as_bytes() - ], + params![validator_id, slot, signing_root.as_bytes()], )?; Ok(()) } @@ -336,19 +412,20 @@ impl SlashingDatabase { &self, txn: &Transaction, validator_pubkey: &PublicKey, - attestation: &AttestationData, - domain: Hash256, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: Hash256, ) -> Result<(), NotSafe> { - let validator_id = Self::get_validator_id(txn, validator_pubkey)?; + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; txn.execute( "INSERT INTO signed_attestations (validator_id, source_epoch, target_epoch, signing_root) VALUES (?1, ?2, ?3, ?4)", params![ validator_id, - attestation.source.epoch, - attestation.target.epoch, - attestation.signing_root(domain).as_bytes() + att_source_epoch, + att_target_epoch, + att_signing_root.as_bytes() ], )?; Ok(()) @@ -365,17 +442,46 @@ impl SlashingDatabase { validator_pubkey: &PublicKey, block_header: &BeaconBlockHeader, domain: Hash256, + ) -> Result<Safe, NotSafe> { + self.check_and_insert_block_signing_root( + validator_pubkey, + block_header.slot, + block_header.signing_root(domain), + ) + } + + /// As for `check_and_insert_block_proposal` but without requiring the whole `BeaconBlockHeader`. + pub fn check_and_insert_block_signing_root( + &self, + validator_pubkey: &PublicKey, + slot: Slot, + signing_root: Hash256, ) -> Result<Safe, NotSafe> { let mut conn = self.conn_pool.get()?; let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; + let safe = self.check_and_insert_block_signing_root_txn( + validator_pubkey, + slot, + signing_root, + &txn, + )?; + txn.commit()?; + Ok(safe) + } - let safe = self.check_block_proposal(&txn, validator_pubkey, block_header, domain)?; + /// Transactional variant of `check_and_insert_block_signing_root`. + pub fn check_and_insert_block_signing_root_txn( + &self, + validator_pubkey: &PublicKey, + slot: Slot, + signing_root: Hash256, + txn: &Transaction, + ) -> Result<Safe, NotSafe> { + let safe = self.check_block_proposal(&txn, validator_pubkey, slot, signing_root)?; if safe != Safe::SameData { - self.insert_block_proposal(&txn, validator_pubkey, block_header, domain)?; + self.insert_block_proposal(&txn, validator_pubkey, slot, signing_root)?; } - - txn.commit()?; Ok(safe) } @@ -390,19 +496,238 @@ impl SlashingDatabase { validator_pubkey: &PublicKey, attestation: &AttestationData, domain: Hash256, + ) -> Result<Safe, NotSafe> { + let attestation_signing_root = attestation.signing_root(domain); + self.check_and_insert_attestation_signing_root( + validator_pubkey, + attestation.source.epoch, + attestation.target.epoch, + attestation_signing_root, + ) + } + + /// As for `check_and_insert_attestation` but without requiring the whole `AttestationData`. + pub fn check_and_insert_attestation_signing_root( + &self, + validator_pubkey: &PublicKey, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: Hash256, ) -> Result<Safe, NotSafe> { let mut conn = self.conn_pool.get()?; let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; - - let safe = self.check_attestation(&txn, validator_pubkey, attestation, domain)?; - - if safe != Safe::SameData { - self.insert_attestation(&txn, validator_pubkey, attestation, domain)?; - } - + let safe = self.check_and_insert_attestation_signing_root_txn( + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + &txn, + )?; txn.commit()?; Ok(safe) } + + /// Transactional variant of `check_and_insert_attestation_signing_root`. + fn check_and_insert_attestation_signing_root_txn( + &self, + validator_pubkey: &PublicKey, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: Hash256, + txn: &Transaction, + ) -> Result<Safe, NotSafe> { + let safe = self.check_attestation( + &txn, + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + )?; + + if safe != Safe::SameData { + self.insert_attestation( + &txn, + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + )?; + } + Ok(safe) + } + + /// Import slashing protection from another client in the interchange format. + pub fn import_interchange_info( + &self, + interchange: &Interchange, + genesis_validators_root: Hash256, + ) -> Result<(), InterchangeError> { + let version = interchange.metadata.interchange_format_version; + if version != SUPPORTED_INTERCHANGE_FORMAT_VERSION { + return Err(InterchangeError::UnsupportedVersion(version)); + } + + if genesis_validators_root != interchange.metadata.genesis_validators_root { + return Err(InterchangeError::GenesisValidatorsMismatch { + client: genesis_validators_root, + interchange_file: interchange.metadata.genesis_validators_root, + }); + } + + // Import atomically, to prevent registering validators with partial information. + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + + for record in &interchange.data { + self.register_validators_in_txn(std::iter::once(&record.pubkey), &txn)?; + + // Insert all signed blocks. + for block in &record.signed_blocks { + self.check_and_insert_block_signing_root_txn( + &record.pubkey, + block.slot, + block.signing_root.unwrap_or_else(Hash256::zero), + &txn, + )?; + } + + // Insert all signed attestations. + for attestation in &record.signed_attestations { + self.check_and_insert_attestation_signing_root_txn( + &record.pubkey, + attestation.source_epoch, + attestation.target_epoch, + attestation.signing_root.unwrap_or_else(Hash256::zero), + &txn, + )?; + } + } + txn.commit()?; + + Ok(()) + } + + pub fn export_interchange_info( + &self, + genesis_validators_root: Hash256, + ) -> Result<Interchange, InterchangeError> { + use std::collections::BTreeMap; + + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + + // Map from internal validator pubkey to blocks and attestation for that pubkey. + let mut data: BTreeMap<String, (Vec<InterchangeBlock>, Vec<InterchangeAttestation>)> = + BTreeMap::new(); + + txn.prepare( + "SELECT public_key, slot, signing_root + FROM signed_blocks, validators + WHERE signed_blocks.validator_id = validators.id", + )? + .query_and_then(params![], |row| { + let validator_pubkey: String = row.get(0)?; + let slot = row.get(1)?; + let signing_root = Some(hash256_from_row(2, &row)?); + let signed_block = InterchangeBlock { slot, signing_root }; + data.entry(validator_pubkey) + .or_insert_with(|| (vec![], vec![])) + .0 + .push(signed_block); + Ok(()) + })? + .collect::<Result<_, InterchangeError>>()?; + + txn.prepare( + "SELECT public_key, source_epoch, target_epoch, signing_root + FROM signed_attestations, validators + WHERE signed_attestations.validator_id = validators.id", + )? + .query_and_then(params![], |row| { + let validator_pubkey: String = row.get(0)?; + let source_epoch = row.get(1)?; + let target_epoch = row.get(2)?; + let signing_root = Some(hash256_from_row(3, &row)?); + let signed_attestation = InterchangeAttestation { + source_epoch, + target_epoch, + signing_root, + }; + data.entry(validator_pubkey) + .or_insert_with(|| (vec![], vec![])) + .1 + .push(signed_attestation); + Ok(()) + })? + .collect::<Result<_, InterchangeError>>()?; + + let metadata = InterchangeMetadata { + interchange_format: InterchangeFormat::Complete, + interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION, + genesis_validators_root, + }; + + let data = data + .into_iter() + .map(|(pubkey, (signed_blocks, signed_attestations))| { + Ok(CompleteInterchangeData { + pubkey: pubkey.parse().map_err(InterchangeError::InvalidPubkey)?, + signed_blocks, + signed_attestations, + }) + }) + .collect::<Result<_, InterchangeError>>()?; + + Ok(Interchange { metadata, data }) + } + + pub fn num_validator_rows(&self) -> Result<u32, NotSafe> { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + let count = txn + .prepare("SELECT COALESCE(COUNT(*), 0) FROM validators")? + .query_row(params![], |row| row.get(0))?; + Ok(count) + } +} + +#[derive(Debug)] +pub enum InterchangeError { + UnsupportedVersion(u64), + GenesisValidatorsMismatch { + interchange_file: Hash256, + client: Hash256, + }, + MinimalAttestationSourceAndTargetInconsistent, + SQLError(String), + SQLPoolError(r2d2::Error), + SerdeJsonError(serde_json::Error), + InvalidPubkey(String), + NotSafe(NotSafe), +} + +impl From<NotSafe> for InterchangeError { + fn from(error: NotSafe) -> Self { + InterchangeError::NotSafe(error) + } +} + +impl From<rusqlite::Error> for InterchangeError { + fn from(error: rusqlite::Error) -> Self { + Self::SQLError(error.to_string()) + } +} + +impl From<r2d2::Error> for InterchangeError { + fn from(error: r2d2::Error) -> Self { + InterchangeError::SQLPoolError(error) + } +} + +impl From<serde_json::Error> for InterchangeError { + fn from(error: serde_json::Error) -> Self { + InterchangeError::SerdeJsonError(error) + } } #[cfg(test)] diff --git a/validator_client/slashing_protection/src/test_utils.rs b/validator_client/slashing_protection/src/test_utils.rs index e95665298..c9320c10d 100644 --- a/validator_client/slashing_protection/src/test_utils.rs +++ b/validator_client/slashing_protection/src/test_utils.rs @@ -1,13 +1,12 @@ -#![cfg(test)] - use crate::*; -use tempfile::tempdir; +use tempfile::{tempdir, TempDir}; use types::{ test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader, Hash256, }; pub const DEFAULT_VALIDATOR_INDEX: usize = 0; pub const DEFAULT_DOMAIN: Hash256 = Hash256::zero(); +pub const DEFAULT_GENESIS_VALIDATORS_ROOT: Hash256 = Hash256::zero(); pub fn pubkey(index: usize) -> PublicKey { generate_deterministic_keypair(index).pk @@ -73,6 +72,16 @@ impl<T> Default for StreamTest<T> { } } +impl<T> StreamTest<T> { + /// The number of test cases that are expected to pass processing successfully. + fn num_expected_successes(&self) -> usize { + self.cases + .iter() + .filter(|case| case.expected.is_ok()) + .count() + } +} + impl StreamTest<AttestationData> { pub fn run(&self) { let dir = tempdir().unwrap(); @@ -91,6 +100,8 @@ impl StreamTest<AttestationData> { i ); } + + roundtrip_database(&dir, &slashing_db, self.num_expected_successes() == 0); } } @@ -112,5 +123,24 @@ impl StreamTest<BeaconBlockHeader> { i ); } + + roundtrip_database(&dir, &slashing_db, self.num_expected_successes() == 0); } } + +fn roundtrip_database(dir: &TempDir, db: &SlashingDatabase, is_empty: bool) { + let exported = db + .export_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) + .unwrap(); + let new_db = + SlashingDatabase::create(&dir.path().join("roundtrip_slashing_protection.sqlite")).unwrap(); + new_db + .import_interchange_info(&exported, DEFAULT_GENESIS_VALIDATORS_ROOT) + .unwrap(); + let reexported = new_db + .export_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) + .unwrap(); + + assert_eq!(exported, reexported); + assert_eq!(is_empty, exported.is_empty()); +} diff --git a/validator_client/slashing_protection/tests/interop.rs b/validator_client/slashing_protection/tests/interop.rs new file mode 100644 index 000000000..c0ea6b8c6 --- /dev/null +++ b/validator_client/slashing_protection/tests/interop.rs @@ -0,0 +1,23 @@ +use slashing_protection::interchange_test::TestCase; +use std::fs::File; +use std::path::PathBuf; + +fn test_root_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("interchange-tests") + .join("tests") +} + +#[test] +fn generated() { + for entry in test_root_dir() + .join("generated") + .read_dir() + .unwrap() + .map(Result::unwrap) + { + let file = File::open(entry.path()).unwrap(); + let test_case: TestCase = serde_json::from_reader(&file).unwrap(); + test_case.run(); + } +} diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 4d230b1b4..1551f1aee 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -10,8 +10,6 @@ use std::path::PathBuf; use types::GRAFFITI_BYTES_LEN; pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/"; -/// Path to the slashing protection database within the datadir. -pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; /// Stores the core configuration for this validator instance. #[derive(Clone, Serialize, Deserialize)] diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 66a616ff3..6bf2f211d 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -1,10 +1,8 @@ use crate::{ - config::{Config, SLASHING_PROTECTION_FILENAME}, - fork_service::ForkService, - initialized_validators::InitializedValidators, + config::Config, fork_service::ForkService, initialized_validators::InitializedValidators, }; use parking_lot::RwLock; -use slashing_protection::{NotSafe, Safe, SlashingDatabase}; +use slashing_protection::{NotSafe, Safe, SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slog::{crit, error, warn, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; From 6ea3bc5e5214c3c625ef60ca4b325073bd0cfa4d Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Fri, 2 Oct 2020 09:42:19 +0000 Subject: [PATCH 07/32] Implement VC API (#1657) ## Issue Addressed NA ## Proposed Changes - Implements a HTTP API for the validator client. - Creates EIP-2335 keystores with an empty `description` field, instead of a missing `description` field. Adds option to set name. - Be more graceful with setups without any validators (yet) - Remove an error log when there are no validators. - Create the `validator` dir if it doesn't exist. - Allow building a `ValidatorDir` without a withdrawal keystore (required for the API method where we only post a voting keystore). - Add optional `description` field to `validator_definitions.yml` ## TODO - [x] Signature header, as per https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855 - [x] Return validator descriptions - [x] Return deposit data - [x] Respect the mnemonic offset - [x] Check that mnemonic can derive returned keys - [x] Be strict about non-localhost - [x] Allow graceful start without any validators (+ create validator dir) - [x] Docs final pass - [x] Swap to EIP-2335 description field. - [x] Fix Zerioze TODO in VC api types. - [x] Zeroize secp256k1 key ## Endpoints - [x] `GET /lighthouse/version` - [x] `GET /lighthouse/health` - [x] `GET /lighthouse/validators` - [x] `POST /lighthouse/validators/hd` - [x] `POST /lighthouse/validators/keystore` - [x] `PATCH /lighthouse/validators/:validator_pubkey` - [ ] ~~`POST /lighthouse/validators/:validator_pubkey/exit/:epoch`~~ Future works ## Additional Info TBC --- Cargo.lock | 105 ++-- account_manager/src/validator/create.rs | 3 +- account_manager/src/validator/recover.rs | 3 +- beacon_node/http_api/src/lib.rs | 21 +- book/src/SUMMARY.md | 3 + book/src/api-vc-auth-header.md | 55 ++ book/src/api-vc-endpoints.md | 363 ++++++++++++ book/src/api-vc-sig-header.md | 108 ++++ book/src/api-vc.md | 37 +- common/account_utils/src/lib.rs | 22 +- .../src/validator_definitions.rs | 4 + common/eth2/Cargo.toml | 6 + common/eth2/src/lib.rs | 15 +- common/eth2/src/lighthouse_vc/http_client.rs | 331 +++++++++++ common/eth2/src/lighthouse_vc/mod.rs | 9 + common/eth2/src/lighthouse_vc/types.rs | 58 ++ common/validator_dir/src/builder.rs | 54 +- common/validator_dir/src/insecure_keys.rs | 3 +- common/validator_dir/src/validator_dir.rs | 5 + common/validator_dir/tests/tests.rs | 68 +-- common/warp_utils/Cargo.toml | 2 + common/warp_utils/src/lib.rs | 1 + common/warp_utils/src/reject.rs | 18 + common/warp_utils/src/task.rs | 21 + crypto/eth2_keystore/src/keystore.rs | 24 +- crypto/eth2_wallet/src/wallet.rs | 17 + lcli/src/insecure_validators.rs | 3 +- lighthouse/environment/Cargo.toml | 2 +- lighthouse/src/main.rs | 2 +- lighthouse/tests/account_manager.rs | 1 + testing/simulator/src/local_network.rs | 2 +- validator_client/Cargo.toml | 10 + validator_client/src/cli.rs | 53 +- validator_client/src/config.rs | 55 +- validator_client/src/fork_service.rs | 79 ++- validator_client/src/http_api/api_secret.rs | 184 ++++++ .../src/http_api/create_validator.rs | 151 +++++ validator_client/src/http_api/mod.rs | 488 ++++++++++++++++ validator_client/src/http_api/tests.rs | 527 ++++++++++++++++++ .../src/initialized_validators.rs | 38 ++ validator_client/src/lib.rs | 53 +- validator_client/src/notifier.rs | 7 +- validator_client/src/validator_store.rs | 43 +- 43 files changed, 2882 insertions(+), 172 deletions(-) create mode 100644 book/src/api-vc-auth-header.md create mode 100644 book/src/api-vc-endpoints.md create mode 100644 book/src/api-vc-sig-header.md create mode 100644 common/eth2/src/lighthouse_vc/http_client.rs create mode 100644 common/eth2/src/lighthouse_vc/mod.rs create mode 100644 common/eth2/src/lighthouse_vc/types.rs create mode 100644 common/warp_utils/src/task.rs create mode 100644 validator_client/src/http_api/api_secret.rs create mode 100644 validator_client/src/http_api/create_validator.rs create mode 100644 validator_client/src/http_api/mod.rs create mode 100644 validator_client/src/http_api/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 34cf85678..256d91740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,15 +720,13 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.18" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d021fddb7bd3e734370acfa4a83f34095571d8570c039f1420d77540f68d5772" +checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" dependencies = [ - "libc", "num-integer", "num-traits", "time 0.1.44", - "winapi 0.3.9", ] [[package]] @@ -790,7 +788,7 @@ dependencies = [ "sloggers", "slot_clock", "store", - "time 0.2.22", + "time 0.2.21", "timer", "tokio 0.2.22", "toml", @@ -1292,6 +1290,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dtoa" version = "0.4.6" @@ -1469,16 +1473,22 @@ dependencies = [ name = "eth2" version = "0.1.0" dependencies = [ + "account_utils", + "bytes 0.5.6", + "eth2_keystore", "eth2_libp2p", "hex 0.4.2", + "libsecp256k1", "procinfo", "proto_array", "psutil", "reqwest", + "ring", "serde", "serde_json", "serde_utils", "types", + "zeroize", ] [[package]] @@ -2157,9 +2167,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" [[package]] name = "hashset_delay" @@ -2543,7 +2553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" dependencies = [ "autocfg 1.0.1", - "hashbrown 0.9.1", + "hashbrown 0.9.0", ] [[package]] @@ -3641,9 +3651,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-src" -version = "111.11.0+1.1.1h" +version = "111.10.2+1.1.1g" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380fe324132bea01f45239fadfec9343adb044615f29930d039bec1ae7b9fa5b" +checksum = "a287fdb22e32b5b60624d4a5a7a02dbe82777f730ec0dbc42a0554326fef5a70" dependencies = [ "cc", ] @@ -3844,18 +3854,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.24" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48fad7cfbff853437be7cf54d7b993af21f53be7f0988cbfe4a51535aa77205" +checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.24" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c6d293bdd3ca5a1697997854c6cf7855e43fb6a0ba1c47af57a5bcafd158ae" +checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" dependencies = [ "proc-macro2", "quote", @@ -3864,9 +3874,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.9" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe74897791e156a0cd8cce0db31b9b2198e67877316bf3086c3acd187f719f0" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" [[package]] name = "pin-utils" @@ -3956,9 +3966,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ef7cd2518ead700af67bf9d1a658d90b6037d77110fd9c0445429d0ba1c6c9" +checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" dependencies = [ "unicode-xid", ] @@ -4060,9 +4070,9 @@ checksum = "cb14183cc7f213ee2410067e1ceeadba2a7478a59432ff0747a335202798b1e2" [[package]] name = "psutil" -version = "3.2.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cdb732329774b8765346796abd1e896e9b3c86aae7f135bb1dda98c2c460f55" +checksum = "094d0f0f32f77f62cd7d137d9b9599ef257d5c1323b36b25746679de2806f547" dependencies = [ "cfg-if", "darwin-libproc", @@ -4073,7 +4083,7 @@ dependencies = [ "num_cpus", "once_cell", "platforms", - "thiserror", + "snafu", "unescape", ] @@ -5066,6 +5076,27 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" +[[package]] +name = "snafu" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c4e6046e4691afe918fd1b603fd6e515bcda5388a1092a9edbada307d159f09" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7073448732a89f2f3e6581989106067f403d378faeafb4a50812eb814170d3e5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "snap" version = "1.0.1" @@ -5297,9 +5328,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.42" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" +checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" dependencies = [ "proc-macro2", "quote", @@ -5441,9 +5472,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.22" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b7151c9065e80917fbf285d9a5d1432f60db41d170ccafc749a136b41a93af" +checksum = "2c2e31fb28e2a9f01f5ed6901b066c1ba2333c04b64dc61254142bafcb3feb2c" dependencies = [ "const_fn", "libc", @@ -5456,9 +5487,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.1.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +checksum = "9ae9b6e9f095bc105e183e3cd493d72579be3181ad4004fceb01adbe9eecab2d" dependencies = [ "proc-macro-hack", "time-macros-impl", @@ -5891,21 +5922,20 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.21" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" dependencies = [ "cfg-if", "log 0.4.11", - "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.17" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" dependencies = [ "lazy_static", ] @@ -6236,13 +6266,19 @@ dependencies = [ "exit-future", "futures 0.3.5", "hex 0.4.2", + "hyper 0.13.8", "libc", + "libsecp256k1", + "lighthouse_version", "logging", "parking_lot 0.11.0", + "rand 0.7.3", "rayon", + "ring", "serde", "serde_derive", "serde_json", + "serde_utils", "serde_yaml", "slashing_protection", "slog", @@ -6250,10 +6286,13 @@ dependencies = [ "slog-term", "slot_clock", "tempdir", + "tempfile", "tokio 0.2.22", "tree_hash", "types", "validator_dir", + "warp", + "warp_utils", ] [[package]] @@ -6369,7 +6408,9 @@ dependencies = [ "beacon_chain", "eth2", "safe_arith", + "serde", "state_processing", + "tokio 0.2.22", "types", "warp", ] diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 9c503ecc6..863cbf1c0 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -218,7 +218,8 @@ pub fn cli_run<T: EthSpec>( ) })?; - ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone()) + ValidatorDirBuilder::new(validator_dir.clone()) + .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) .create_eth1_tx_data(deposit_gwei, &spec) diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index e3844d500..ecea0efbd 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -124,7 +124,8 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let voting_pubkey = keystores.voting.pubkey().to_string(); - ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone()) + ValidatorDirBuilder::new(validator_dir.clone()) + .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) .store_withdrawal_keystore(matches.is_present(STORE_WITHDRAW_FLAG)) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index b23937b5d..1b52cbd2c 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -42,6 +42,7 @@ use types::{ SignedBeaconBlock, SignedVoluntaryExit, Slot, YamlConfig, }; use warp::Filter; +use warp_utils::task::{blocking_json_task, blocking_task}; const API_PREFIX: &str = "eth"; const API_VERSION: &str = "v1"; @@ -1727,23 +1728,3 @@ fn publish_network_message<T: EthSpec>( )) }) } - -/// Execute some task in a tokio "blocking thread". These threads are ideal for long-running -/// (blocking) tasks since they don't jam up the core executor. -async fn blocking_task<F, T>(func: F) -> T -where - F: Fn() -> T, -{ - tokio::task::block_in_place(func) -} - -/// A convenience wrapper around `blocking_task` for use with `warp` JSON responses. -async fn blocking_json_task<F, T>(func: F) -> Result<warp::reply::Json, warp::Rejection> -where - F: Fn() -> Result<T, warp::Rejection>, - T: Serialize, -{ - blocking_task(func) - .await - .map(|resp| warp::reply::json(&resp)) -} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index b570357b9..a13ced95a 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -19,6 +19,9 @@ * [/lighthouse](./api-lighthouse.md) * [Validator Inclusion APIs](./validator-inclusion.md) * [Validator Client API](./api-vc.md) + * [Endpoints](./api-vc-endpoints.md) + * [Authorization Header](./api-vc-auth-header.md) + * [Signature Header](./api-vc-sig-header.md) * [Prometheus Metrics](./advanced_metrics.md) * [Advanced Usage](./advanced.md) * [Database Configuration](./advanced_database.md) diff --git a/book/src/api-vc-auth-header.md b/book/src/api-vc-auth-header.md new file mode 100644 index 000000000..dbd334c9c --- /dev/null +++ b/book/src/api-vc-auth-header.md @@ -0,0 +1,55 @@ +# Validator Client API: Authorization Header + +## Overview + +The validator client HTTP server requires that all requests have the following +HTTP header: + +- Name: `Authorization` +- Value: `Basic <api-token>` + +Where `<api-token>` is a string that can be obtained from the validator client +host. Here is an example `Authorization` header: + +``` +Authorization Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123 +``` + +## Obtaining the API token + +The API token can be obtained via two methods: + +### Method 1: Reading from a file + +The API token is stored as a file in the `validators` directory. For most users +this is `~/.lighthouse/{testnet}/validators/api-token.txt`. Here's an +example using the `cat` command to print the token to the terminal, but any +text editor will suffice: + +``` +$ cat api-token.txt +api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123 +``` + +### Method 2: Reading from logs + +When starting the validator client it will output a log message containing an +`api-token` field: + +``` +Sep 28 19:17:52.615 INFO HTTP API started api_token: api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123, listen_address: 127.0.0.1:5062 +``` + +## Example + +Here is an example `curl` command using the API token in the `Authorization` header: + +```bash +curl localhost:5062/lighthouse/version -H "Authorization: Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123" +``` + +The server should respond with its version: + +```json +{"data":{"version":"Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux"}} +``` diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md new file mode 100644 index 000000000..78ea49356 --- /dev/null +++ b/book/src/api-vc-endpoints.md @@ -0,0 +1,363 @@ +# Validator Client API: Endpoints + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`GET /lighthouse/version`](#get-lighthouseversion) | Get the Lighthouse software version +[`GET /lighthouse/health`](#get-lighthousehealth) | Get information about the host machine +[`GET /lighthouse/spec`](#get-lighthousespec) | Get the Eth2 specification used by the validator +[`GET /lighthouse/validators`](#get-lighthousevalidators) | List all validators +[`GET /lighthouse/validators/:voting_pubkey`](#get-lighthousevalidatorsvoting_pubkey) | Get a specific validator +[`PATCH /lighthouse/validators/:voting_pubkey`](#patch-lighthousevalidatorsvoting_pubkey) | Update a specific validator +[`POST /lighthouse/validators`](#post-lighthousevalidators) | Create a new validator and mnemonic. +[`POST /lighthouse/validators/mnemonic`](#post-lighthousevalidatorsmnemonic) | Create a new validator from an existing mnemonic. + +## `GET /lighthouse/version` + +Returns the software version and `git` commit hash for the Lighthouse binary. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/version` +Method | GET +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200 + +### Example Response Body + +```json +{ + "data": { + "version": "Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux" + } +} +``` + +## `GET /lighthouse/health` + +Returns information regarding the health of the host machine. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/health` +Method | GET +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200 + +*Note: this endpoint is presently only available on Linux.* + +### Example Response Body + +```json +{ + "data": { + "pid": 1476293, + "pid_num_threads": 19, + "pid_mem_resident_set_size": 4009984, + "pid_mem_virtual_memory_size": 1306775552, + "sys_virt_mem_total": 33596100608, + "sys_virt_mem_available": 23073017856, + "sys_virt_mem_used": 9346957312, + "sys_virt_mem_free": 22410510336, + "sys_virt_mem_percent": 31.322334, + "sys_loadavg_1": 0.98, + "sys_loadavg_5": 0.98, + "sys_loadavg_15": 1.01 + } +} +``` + +## `GET /lighthouse/spec` + +Returns the Eth2 specification loaded for this validator. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/spec` +Method | GET +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200 + +### Example Response Body + +```json +{ + "data": { + "CONFIG_NAME": "mainnet", + "MAX_COMMITTEES_PER_SLOT": "64", + "TARGET_COMMITTEE_SIZE": "128", + "MIN_PER_EPOCH_CHURN_LIMIT": "4", + "CHURN_LIMIT_QUOTIENT": "65536", + "SHUFFLE_ROUND_COUNT": "90", + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "1024", + "MIN_GENESIS_TIME": "1601380800", + "GENESIS_DELAY": "172800", + "MIN_DEPOSIT_AMOUNT": "1000000000", + "MAX_EFFECTIVE_BALANCE": "32000000000", + "EJECTION_BALANCE": "16000000000", + "EFFECTIVE_BALANCE_INCREMENT": "1000000000", + "HYSTERESIS_QUOTIENT": "4", + "HYSTERESIS_DOWNWARD_MULTIPLIER": "1", + "HYSTERESIS_UPWARD_MULTIPLIER": "5", + "PROPORTIONAL_SLASHING_MULTIPLIER": "3", + "GENESIS_FORK_VERSION": "0x00000002", + "BLS_WITHDRAWAL_PREFIX": "0x00", + "SECONDS_PER_SLOT": "12", + "MIN_ATTESTATION_INCLUSION_DELAY": "1", + "MIN_SEED_LOOKAHEAD": "1", + "MAX_SEED_LOOKAHEAD": "4", + "MIN_EPOCHS_TO_INACTIVITY_PENALTY": "4", + "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", + "SHARD_COMMITTEE_PERIOD": "256", + "BASE_REWARD_FACTOR": "64", + "WHISTLEBLOWER_REWARD_QUOTIENT": "512", + "PROPOSER_REWARD_QUOTIENT": "8", + "INACTIVITY_PENALTY_QUOTIENT": "16777216", + "MIN_SLASHING_PENALTY_QUOTIENT": "32", + "SAFE_SLOTS_TO_UPDATE_JUSTIFIED": "8", + "DOMAIN_BEACON_PROPOSER": "0x00000000", + "DOMAIN_BEACON_ATTESTER": "0x01000000", + "DOMAIN_RANDAO": "0x02000000", + "DOMAIN_DEPOSIT": "0x03000000", + "DOMAIN_VOLUNTARY_EXIT": "0x04000000", + "DOMAIN_SELECTION_PROOF": "0x05000000", + "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", + "MAX_VALIDATORS_PER_COMMITTEE": "2048", + "SLOTS_PER_EPOCH": "32", + "EPOCHS_PER_ETH1_VOTING_PERIOD": "32", + "SLOTS_PER_HISTORICAL_ROOT": "8192", + "EPOCHS_PER_HISTORICAL_VECTOR": "65536", + "EPOCHS_PER_SLASHINGS_VECTOR": "8192", + "HISTORICAL_ROOTS_LIMIT": "16777216", + "VALIDATOR_REGISTRY_LIMIT": "1099511627776", + "MAX_PROPOSER_SLASHINGS": "16", + "MAX_ATTESTER_SLASHINGS": "2", + "MAX_ATTESTATIONS": "128", + "MAX_DEPOSITS": "16", + "MAX_VOLUNTARY_EXITS": "16", + "ETH1_FOLLOW_DISTANCE": "1024", + "TARGET_AGGREGATORS_PER_COMMITTEE": "16", + "RANDOM_SUBNETS_PER_VALIDATOR": "1", + "EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION": "256", + "SECONDS_PER_ETH1_BLOCK": "14", + "DEPOSIT_CONTRACT_ADDRESS": "0x48b597f4b53c21b48ad95c7256b49d1779bd5890" + } +} +``` + +## `GET /lighthouse/validators` + +Lists all validators managed by this validator client. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/validators` +Method | GET +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200 + +### Example Response Body + +```json +{ + "data": [ + { + "enabled": true, + "voting_pubkey": "0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" + }, + { + "enabled": true, + "voting_pubkey": "0xb0441246ed813af54c0a11efd53019f63dd454a1fa2a9939ce3c228419fbe113fb02b443ceeb38736ef97877eb88d43a" + }, + { + "enabled": true, + "voting_pubkey": "0xad77e388d745f24e13890353031dd8137432ee4225752642aad0a2ab003c86620357d91973b6675932ff51f817088f38" + } + ] +} +``` + +## `GET /lighthouse/validators/:voting_pubkey` + +Get a validator by their `voting_pubkey`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/validators/:voting_pubkey` +Method | GET +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200, 400 + +### Example Path + +``` +localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde +``` + +### Example Response Body + +```json +{ + "data": { + "enabled": true, + "voting_pubkey": "0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" + } +} +``` + +## `PATCH /lighthouse/validators/:voting_pubkey` + +Update some values for the validator with `voting_pubkey`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/validators/:voting_pubkey` +Method | PATCH +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200, 400 + +### Example Path + +``` +localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde +``` + +### Example Request Body + +```json +{ + "enabled": false +} +``` + +### Example Response Body + +```json +null +``` + +## `POST /lighthouse/validators/` + +Create any number of new validators, all of which will share a common mnemonic +generated by the server. + +A BIP-39 mnemonic will be randomly generated and returned with the response. +This mnemonic can be used to recover all keys returned in the response. +Validators are generated from the mnemonic according to +[EIP-2334](https://eips.ethereum.org/EIPS/eip-2334), starting at index `0`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/validators` +Method | POST +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200 + +### Example Request Body + +```json +[ + { + "enable": true, + "description": "validator_one", + "deposit_gwei": "32000000000" + }, + { + "enable": false, + "description": "validator two", + "deposit_gwei": "34000000000" + } +] +``` + +### Example Response Body + +```json +{ + "data": { + "mnemonic": "marine orchard scout label trim only narrow taste art belt betray soda deal diagram glare hero scare shadow ramp blur junior behave resource tourist", + "validators": [ + { + "enabled": true, + "description": "validator_one", + "voting_pubkey": "0x8ffbc881fb60841a4546b4b385ec5e9b5090fd1c4395e568d98b74b94b41a912c6101113da39d43c101369eeb9b48e50", + "eth1_deposit_tx_data": "0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001206c68675776d418bfd63468789e7c68a6788c4dd45a3a911fe3d642668220bbf200000000000000000000000000000000000000000000000000000000000000308ffbc881fb60841a4546b4b385ec5e9b5090fd1c4395e568d98b74b94b41a912c6101113da39d43c101369eeb9b48e5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000cf8b3abbf0ecd91f3b0affcc3a11e9c5f8066efb8982d354ee9a812219b17000000000000000000000000000000000000000000000000000000000000000608fbe2cc0e17a98d4a58bd7a65f0475a58850d3c048da7b718f8809d8943fee1dbd5677c04b5fa08a9c44d271d009edcd15caa56387dc217159b300aad66c2cf8040696d383d0bff37b2892a7fe9ba78b2220158f3dc1b9cd6357bdcaee3eb9f2", + "deposit_gwei": "32000000000" + }, + { + "enabled": false, + "description": "validator two", + "voting_pubkey": "0xa9fadd620dc68e9fe0d6e1a69f6c54a0271ad65ab5a509e645e45c6e60ff8f4fc538f301781193a08b55821444801502", + "eth1_deposit_tx_data": "0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120b1911954c1b8d23233e0e2bf8c4878c8f56d25a4f790ec09a94520ec88af30490000000000000000000000000000000000000000000000000000000000000030a9fadd620dc68e9fe0d6e1a69f6c54a0271ad65ab5a509e645e45c6e60ff8f4fc538f301781193a08b5582144480150200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000a96df8b95c3ba749265e48a101f2ed974fffd7487487ed55f8dded99b617ad000000000000000000000000000000000000000000000000000000000000006090421299179824950e2f5a592ab1fdefe5349faea1e8126146a006b64777b74cce3cfc5b39d35b370e8f844e99c2dc1b19a1ebd38c7605f28e9c4540aea48f0bc48e853ae5f477fa81a9fc599d1732968c772730e1e47aaf5c5117bd045b788e", + "deposit_gwei": "34000000000" + } + ] + } +} +``` + +## `POST /lighthouse/validators/mnemonic` + +Create any number of new validators, all of which will share a common mnemonic. + +The supplied BIP-39 mnemonic will be used to generate the validator keys +according to [EIP-2334](https://eips.ethereum.org/EIPS/eip-2334), starting at +the supplied `key_derivation_path_offset`. For example, if +`key_derivation_path_offset = 42`, then the first validator voting key will be +generated with the path `m/12381/3600/i/42`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/validators/mnemonic` +Method | POST +Required Headers | [`Authorization`](./api-vc-auth-header.md) +Typical Responses | 200 + +### Example Request Body + +```json +{ + "mnemonic": "theme onion deal plastic claim silver fancy youth lock ordinary hotel elegant balance ridge web skill burger survey demand distance legal fish salad cloth", + "key_derivation_path_offset": 0, + "validators": [ + { + "enable": true, + "description": "validator_one", + "deposit_gwei": "32000000000" + } + ] +} +``` + +### Example Response Body + +```json +{ + "data": [ + { + "enabled": true, + "description": "validator_one", + "voting_pubkey": "0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380", + "eth1_deposit_tx_data": "0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120a57324d95ae9c7abfb5cc9bd4db253ed0605dc8a19f84810bcf3f3874d0e703a0000000000000000000000000000000000000000000000000000000000000030a062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db3800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200046e4199f18102b5d4e8842d0eeafaa1268ee2c21340c63f9c2cd5b03ff19320000000000000000000000000000000000000000000000000000000000000060b2a897b4ba4f3910e9090abc4c22f81f13e8923ea61c0043506950b6ae174aa643540554037b465670d28fa7b7d716a301e9b172297122acc56be1131621c072f7c0a73ea7b8c5a90ecd5da06d79d90afaea17cdeeef8ed323912c70ad62c04b", + "deposit_gwei": "32000000000" + } + ] +} +``` diff --git a/book/src/api-vc-sig-header.md b/book/src/api-vc-sig-header.md new file mode 100644 index 000000000..a1b9b104f --- /dev/null +++ b/book/src/api-vc-sig-header.md @@ -0,0 +1,108 @@ +# Validator Client API: Signature Header + +## Overview + +The validator client HTTP server adds the following header to all responses: + +- Name: `Signature` +- Value: a secp256k1 signature across the SHA256 of the response body. + +Example `Signature` header: + +``` +Signature: 0x304402205b114366444112580bf455d919401e9c869f5af067cd496016ab70d428b5a99d0220067aede1eb5819eecfd5dd7a2b57c5ac2b98f25a7be214b05684b04523aef873 +``` + +## Verifying the Signature + +Below is a browser-ready example of signature verification. + +### HTML + +```html +<script src="https://rawgit.com/emn178/js-sha256/master/src/sha256.js" type="text/javascript"></script> +<script src="https://rawgit.com/indutny/elliptic/master/dist/elliptic.min.js" type="text/javascript"></script> +``` + +### Javascript + +```javascript +// Helper function to turn a hex-string into bytes. +function hexStringToByte(str) { + if (!str) { + return new Uint8Array(); + } + + var a = []; + for (var i = 0, len = str.length; i < len; i+=2) { + a.push(parseInt(str.substr(i,2),16)); + } + + return new Uint8Array(a); +} + +// This example uses the secp256k1 curve from the "elliptic" library: +// +// https://github.com/indutny/elliptic +var ec = new elliptic.ec('secp256k1'); + +// The public key is contained in the API token: +// +// Authorization: Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123 +var pk_bytes = hexStringToByte('03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123'); + +// The signature is in the `Signature` header of the response: +// +// Signature: 0x304402205b114366444112580bf455d919401e9c869f5af067cd496016ab70d428b5a99d0220067aede1eb5819eecfd5dd7a2b57c5ac2b98f25a7be214b05684b04523aef873 +var sig_bytes = hexStringToByte('304402205b114366444112580bf455d919401e9c869f5af067cd496016ab70d428b5a99d0220067aede1eb5819eecfd5dd7a2b57c5ac2b98f25a7be214b05684b04523aef873'); + +// The HTTP response body. +var response_body = "{\"data\":{\"version\":\"Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux\"}}"; + +// The HTTP response body is hashed (SHA256) to determine the 32-byte message. +let hash = sha256.create(); +hash.update(response_body); +let message = hash.array(); + +// The 32-byte message hash, the signature and the public key are verified. +if (ec.verify(message, sig_bytes, pk_bytes)) { + console.log("The signature is valid") +} else { + console.log("The signature is invalid") +} +``` + +*This example is also available as a [JSFiddle](https://jsfiddle.net/wnqd74Lz/).* + +## Example + +The previous Javascript example was written using the output from the following +`curl` command: + +```bash +curl -v localhost:5062/lighthouse/version -H "Authorization: Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123" +``` + +``` +* Trying ::1:5062... +* connect to ::1 port 5062 failed: Connection refused +* Trying 127.0.0.1:5062... +* Connected to localhost (127.0.0.1) port 5062 (#0) +> GET /lighthouse/version HTTP/1.1 +> Host: localhost:5062 +> User-Agent: curl/7.72.0 +> Accept: */* +> Authorization: Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< signature: 0x304402205b114366444112580bf455d919401e9c869f5af067cd496016ab70d428b5a99d0220067aede1eb5819eecfd5dd7a2b57c5ac2b98f25a7be214b05684b04523aef873 +< server: Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux +< access-control-allow-origin: +< content-length: 65 +< date: Tue, 29 Sep 2020 04:23:46 GMT +< +* Connection #0 to host localhost left intact +{"data":{"version":"Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux"}} +``` diff --git a/book/src/api-vc.md b/book/src/api-vc.md index e120f69bf..0a8941eda 100644 --- a/book/src/api-vc.md +++ b/book/src/api-vc.md @@ -1,3 +1,38 @@ # Validator Client API -The validator client API is planned for release in late September 2020. +Lighthouse implements a HTTP/JSON API for the validator client. Since there is +no Eth2 standard validator client API, Lighthouse has defined its own. + +A full list of endpoints can be found in [Endpoints](./api-vc-endpoints.md). + +> Note: All requests to the HTTP server must supply an +> [`Authorization`](./api-vc-auth-header.md) header. All responses contain a +> [`Signature`](./api-vc-sig-header.md) header for optional verification. + +## Starting the server + +A Lighthouse validator client can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `127.0.0.1:5062`. + +The following CLI flags control the HTTP server: + +- `--http`: enable the HTTP server (required even if the following flags are + provided). +- `--http-port`: specify the listen port of the server. +- `--http-allow-origin`: specify the value of the `Access-Control-Allow-Origin` + header. The default is to not supply a header. + +## Security + +The validator client HTTP is **not encrypted** (i.e., it is **not HTTPS**). For +this reason, it will only listen on `127.0.0.1`. + +It is unsafe to expose the validator client to the public Internet without +additional transport layer security (e.g., HTTPS via nginx, SSH tunnels, etc.). + +### CLI Example + +Start the validator client with the HTTP server listening on [http://localhost:5062](http://localhost:5062): + +```bash +lighthouse vc --http +``` diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 77351a7b9..d74ed71ed 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -2,7 +2,10 @@ //! Lighthouse project. use eth2_keystore::Keystore; -use eth2_wallet::Wallet; +use eth2_wallet::{ + bip39::{Language, Mnemonic, MnemonicType}, + Wallet, +}; use rand::{distributions::Alphanumeric, Rng}; use serde_derive::{Deserialize, Serialize}; use std::fs::{self, File}; @@ -15,6 +18,7 @@ use zeroize::Zeroize; pub mod validator_definitions; pub use eth2_keystore; +pub use eth2_wallet; pub use eth2_wallet::PlainText; /// The minimum number of characters required for a wallet password. @@ -150,6 +154,16 @@ pub fn is_password_sufficiently_complex(password: &[u8]) -> Result<(), String> { } } +/// Returns a random 24-word english mnemonic. +pub fn random_mnemonic() -> Mnemonic { + Mnemonic::new(MnemonicType::Words24, Language::English) +} + +/// Attempts to parse a mnemonic phrase. +pub fn mnemonic_from_phrase(phrase: &str) -> Result<Mnemonic, String> { + Mnemonic::from_phrase(phrase, Language::English).map_err(|e| e.to_string()) +} + /// Provides a new-type wrapper around `String` that is zeroized on `Drop`. /// /// Useful for ensuring that password memory is zeroed-out on drop. @@ -164,6 +178,12 @@ impl From<String> for ZeroizeString { } } +impl ZeroizeString { + pub fn as_str(&self) -> &str { + &self.0 + } +} + impl AsRef<[u8]> for ZeroizeString { fn as_ref(&self) -> &[u8] { self.0.as_bytes() diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 733c771be..b69678628 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -63,6 +63,8 @@ pub enum SigningDefinition { pub struct ValidatorDefinition { pub enabled: bool, pub voting_public_key: PublicKey, + #[serde(default)] + pub description: String, #[serde(flatten)] pub signing_definition: SigningDefinition, } @@ -88,6 +90,7 @@ impl ValidatorDefinition { Ok(ValidatorDefinition { enabled: true, voting_public_key, + description: keystore.description().unwrap_or_else(|| "").to_string(), signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, @@ -205,6 +208,7 @@ impl ValidatorDefinitions { Some(ValidatorDefinition { enabled: true, voting_public_key, + description: keystore.description().unwrap_or_else(|| "").to_string(), signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path, diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index f7ccfcf34..479630c9c 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -15,6 +15,12 @@ reqwest = { version = "0.10.8", features = ["json"] } eth2_libp2p = { path = "../../beacon_node/eth2_libp2p" } proto_array = { path = "../../consensus/proto_array", optional = true } serde_utils = { path = "../../consensus/serde_utils" } +zeroize = { version = "1.0.0", features = ["zeroize_derive"] } +eth2_keystore = { path = "../../crypto/eth2_keystore" } +libsecp256k1 = "0.3.5" +ring = "0.16.12" +bytes = "0.5.6" +account_utils = { path = "../../common/account_utils" } [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.1.0", optional = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index b0fbc2566..8e9a18b47 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -9,6 +9,7 @@ #[cfg(feature = "lighthouse")] pub mod lighthouse; +pub mod lighthouse_vc; pub mod types; use self::types::*; @@ -30,6 +31,14 @@ pub enum Error { StatusCode(StatusCode), /// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`. InvalidUrl(Url), + /// The supplied validator client secret is invalid. + InvalidSecret(String), + /// The server returned a response with an invalid signature. It may be an impostor. + InvalidSignatureHeader, + /// The server returned a response without a signature header. It may be an impostor. + MissingSignatureHeader, + /// The server returned an invalid JSON response. + InvalidJson(serde_json::Error), } impl Error { @@ -40,6 +49,10 @@ impl Error { Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), Error::InvalidUrl(_) => None, + Error::InvalidSecret(_) => None, + Error::InvalidSignatureHeader => None, + Error::MissingSignatureHeader => None, + Error::InvalidJson(_) => None, } } } @@ -531,7 +544,7 @@ impl BeaconNodeHttpClient { self.get(path).await } - /// `GET config/fork_schedule` + /// `GET config/spec` pub async fn get_config_spec(&self) -> Result<GenericResponse<YamlConfig>, Error> { let mut path = self.eth_path()?; diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs new file mode 100644 index 000000000..b08ceabb2 --- /dev/null +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -0,0 +1,331 @@ +use super::{types::*, PK_LEN, SECRET_PREFIX}; +use crate::Error; +use account_utils::ZeroizeString; +use bytes::Bytes; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + IntoUrl, +}; +use ring::digest::{digest, SHA256}; +use secp256k1::{Message, PublicKey, Signature}; +use serde::{de::DeserializeOwned, Serialize}; + +pub use reqwest; +pub use reqwest::{Response, StatusCode, Url}; + +/// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a +/// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). +#[derive(Clone)] +pub struct ValidatorClientHttpClient { + client: reqwest::Client, + server: Url, + secret: ZeroizeString, + server_pubkey: PublicKey, +} + +/// Parse an API token and return a secp256k1 public key. +pub fn parse_pubkey(secret: &str) -> Result<PublicKey, Error> { + let secret = if !secret.starts_with(SECRET_PREFIX) { + return Err(Error::InvalidSecret(format!( + "secret does not start with {}", + SECRET_PREFIX + ))); + } else { + &secret[SECRET_PREFIX.len()..] + }; + + serde_utils::hex::decode(&secret) + .map_err(|e| Error::InvalidSecret(format!("invalid hex: {:?}", e))) + .and_then(|bytes| { + if bytes.len() != PK_LEN { + return Err(Error::InvalidSecret(format!( + "expected {} bytes not {}", + PK_LEN, + bytes.len() + ))); + } + + let mut arr = [0; PK_LEN]; + arr.copy_from_slice(&bytes); + PublicKey::parse_compressed(&arr) + .map_err(|e| Error::InvalidSecret(format!("invalid secp256k1 pubkey: {:?}", e))) + }) +} + +impl ValidatorClientHttpClient { + pub fn new(server: Url, secret: String) -> Result<Self, Error> { + Ok(Self { + client: reqwest::Client::new(), + server, + server_pubkey: parse_pubkey(&secret)?, + secret: secret.into(), + }) + } + + pub fn from_components( + server: Url, + client: reqwest::Client, + secret: String, + ) -> Result<Self, Error> { + Ok(Self { + client, + server, + server_pubkey: parse_pubkey(&secret)?, + secret: secret.into(), + }) + } + + async fn signed_body(&self, response: Response) -> Result<Bytes, Error> { + let sig = response + .headers() + .get("Signature") + .ok_or_else(|| Error::MissingSignatureHeader)? + .to_str() + .map_err(|_| Error::InvalidSignatureHeader)? + .to_string(); + + let body = response.bytes().await.map_err(Error::Reqwest)?; + + let message = + Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes"); + + serde_utils::hex::decode(&sig) + .ok() + .and_then(|bytes| { + let sig = Signature::parse_der(&bytes).ok()?; + Some(secp256k1::verify(&message, &sig, &self.server_pubkey)) + }) + .filter(|is_valid| *is_valid) + .ok_or_else(|| Error::InvalidSignatureHeader)?; + + Ok(body) + } + + async fn signed_json<T: DeserializeOwned>(&self, response: Response) -> Result<T, Error> { + let body = self.signed_body(response).await?; + serde_json::from_slice(&body).map_err(Error::InvalidJson) + } + + fn headers(&self) -> Result<HeaderMap, Error> { + let header_value = HeaderValue::from_str(&format!("Basic {}", self.secret.as_str())) + .map_err(|e| { + Error::InvalidSecret(format!("secret is invalid as a header value: {}", e)) + })?; + + let mut headers = HeaderMap::new(); + headers.insert("Authorization", header_value); + + Ok(headers) + } + + /// Perform a HTTP GET request. + async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> { + let response = self + .client + .get(url) + .headers(self.headers()?) + .send() + .await + .map_err(Error::Reqwest)?; + let response = ok_or_error(response).await?; + self.signed_json(response).await + } + + /// Perform a HTTP GET request, returning `None` on a 404 error. + async fn get_opt<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<Option<T>, Error> { + let response = self + .client + .get(url) + .headers(self.headers()?) + .send() + .await + .map_err(Error::Reqwest)?; + match ok_or_error(response).await { + Ok(resp) => self.signed_json(resp).await.map(Option::Some), + Err(err) => { + if err.status() == Some(StatusCode::NOT_FOUND) { + Ok(None) + } else { + Err(err) + } + } + } + } + + /// Perform a HTTP POST request. + async fn post<T: Serialize, U: IntoUrl, V: DeserializeOwned>( + &self, + url: U, + body: &T, + ) -> Result<V, Error> { + let response = self + .client + .post(url) + .headers(self.headers()?) + .json(body) + .send() + .await + .map_err(Error::Reqwest)?; + let response = ok_or_error(response).await?; + self.signed_json(response).await + } + + /// Perform a HTTP PATCH request. + async fn patch<T: Serialize, U: IntoUrl>(&self, url: U, body: &T) -> Result<(), Error> { + let response = self + .client + .patch(url) + .headers(self.headers()?) + .json(body) + .send() + .await + .map_err(Error::Reqwest)?; + let response = ok_or_error(response).await?; + self.signed_body(response).await?; + Ok(()) + } + + /// `GET lighthouse/version` + pub async fn get_lighthouse_version(&self) -> Result<GenericResponse<VersionData>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("version"); + + self.get(path).await + } + + /// `GET lighthouse/health` + pub async fn get_lighthouse_health(&self) -> Result<GenericResponse<Health>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("health"); + + self.get(path).await + } + + /// `GET lighthouse/spec` + pub async fn get_lighthouse_spec(&self) -> Result<GenericResponse<YamlConfig>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("spec"); + + self.get(path).await + } + + /// `GET lighthouse/validators` + pub async fn get_lighthouse_validators( + &self, + ) -> Result<GenericResponse<Vec<ValidatorData>>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validators"); + + self.get(path).await + } + + /// `GET lighthouse/validators/{validator_pubkey}` + pub async fn get_lighthouse_validators_pubkey( + &self, + validator_pubkey: &PublicKeyBytes, + ) -> Result<Option<GenericResponse<ValidatorData>>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validators") + .push(&validator_pubkey.to_string()); + + self.get_opt(path).await + } + + /// `POST lighthouse/validators` + pub async fn post_lighthouse_validators( + &self, + validators: Vec<ValidatorRequest>, + ) -> Result<GenericResponse<PostValidatorsResponseData>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validators"); + + self.post(path, &validators).await + } + + /// `POST lighthouse/validators/mnemonic` + pub async fn post_lighthouse_validators_mnemonic( + &self, + request: &CreateValidatorsMnemonicRequest, + ) -> Result<GenericResponse<Vec<CreatedValidator>>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validators") + .push("mnemonic"); + + self.post(path, &request).await + } + + /// `POST lighthouse/validators/keystore` + pub async fn post_lighthouse_validators_keystore( + &self, + request: &KeystoreValidatorsPostRequest, + ) -> Result<GenericResponse<ValidatorData>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validators") + .push("keystore"); + + self.post(path, &request).await + } + + /// `PATCH lighthouse/validators/{validator_pubkey}` + pub async fn patch_lighthouse_validators( + &self, + voting_pubkey: &PublicKeyBytes, + enabled: bool, + ) -> Result<(), Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("validators") + .push(&voting_pubkey.to_string()); + + self.patch(path, &ValidatorPatchRequest { enabled }).await + } +} + +/// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an +/// appropriate error message. +async fn ok_or_error(response: Response) -> Result<Response, Error> { + let status = response.status(); + + if status == StatusCode::OK { + Ok(response) + } else if let Ok(message) = response.json().await { + Err(Error::ServerMessage(message)) + } else { + Err(Error::StatusCode(status)) + } +} diff --git a/common/eth2/src/lighthouse_vc/mod.rs b/common/eth2/src/lighthouse_vc/mod.rs new file mode 100644 index 000000000..b7de7c715 --- /dev/null +++ b/common/eth2/src/lighthouse_vc/mod.rs @@ -0,0 +1,9 @@ +pub mod http_client; +pub mod types; + +/// The number of bytes in the secp256k1 public key used as the authorization token for the VC API. +pub const PK_LEN: usize = 33; + +/// The prefix for the secp256k1 public key when it is used as the authorization token for the VC +/// API. +pub const SECRET_PREFIX: &str = "api-token-"; diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs new file mode 100644 index 000000000..64674e6fc --- /dev/null +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -0,0 +1,58 @@ +use account_utils::ZeroizeString; +use eth2_keystore::Keystore; +use serde::{Deserialize, Serialize}; + +pub use crate::lighthouse::Health; +pub use crate::types::{GenericResponse, VersionData}; +pub use types::*; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorData { + pub enabled: bool, + pub description: String, + pub voting_pubkey: PublicKeyBytes, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorRequest { + pub enable: bool, + pub description: String, + #[serde(with = "serde_utils::quoted_u64")] + pub deposit_gwei: u64, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct CreateValidatorsMnemonicRequest { + pub mnemonic: ZeroizeString, + #[serde(with = "serde_utils::quoted_u32")] + pub key_derivation_path_offset: u32, + pub validators: Vec<ValidatorRequest>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CreatedValidator { + pub enabled: bool, + pub description: String, + pub voting_pubkey: PublicKeyBytes, + pub eth1_deposit_tx_data: String, + #[serde(with = "serde_utils::quoted_u64")] + pub deposit_gwei: u64, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct PostValidatorsResponseData { + pub mnemonic: ZeroizeString, + pub validators: Vec<CreatedValidator>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorPatchRequest { + pub enabled: bool, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct KeystoreValidatorsPostRequest { + pub password: ZeroizeString, + pub enable: bool, + pub keystore: Keystore, +} diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 4cdb75a5f..ad1b01ee4 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -40,6 +40,7 @@ pub enum Error { UninitializedWithdrawalKeystore, #[cfg(feature = "insecure_keys")] InsecureKeysError(String), + MissingPasswordDir, } impl From<KeystoreError> for Error { @@ -51,7 +52,7 @@ impl From<KeystoreError> for Error { /// A builder for creating a `ValidatorDir`. pub struct Builder<'a> { base_validators_dir: PathBuf, - password_dir: PathBuf, + password_dir: Option<PathBuf>, pub(crate) voting_keystore: Option<(Keystore, PlainText)>, pub(crate) withdrawal_keystore: Option<(Keystore, PlainText)>, store_withdrawal_keystore: bool, @@ -60,10 +61,10 @@ pub struct Builder<'a> { impl<'a> Builder<'a> { /// Instantiate a new builder. - pub fn new(base_validators_dir: PathBuf, password_dir: PathBuf) -> Self { + pub fn new(base_validators_dir: PathBuf) -> Self { Self { base_validators_dir, - password_dir, + password_dir: None, voting_keystore: None, withdrawal_keystore: None, store_withdrawal_keystore: true, @@ -71,6 +72,12 @@ impl<'a> Builder<'a> { } } + /// Supply a directory in which to store the passwords for the validator keystores. + pub fn password_dir<P: Into<PathBuf>>(mut self, password_dir: P) -> Self { + self.password_dir = Some(password_dir.into()); + self + } + /// Build the `ValidatorDir` use the given `keystore` which can be unlocked with `password`. /// /// The builder will not necessarily check that `password` can unlock `keystore`. @@ -215,26 +222,35 @@ impl<'a> Builder<'a> { } } - // Only the withdrawal keystore if explicitly required. - if self.store_withdrawal_keystore { - // Write the withdrawal password to file. - write_password_to_file( - self.password_dir - .join(withdrawal_keypair.pk.to_hex_string()), - withdrawal_password.as_bytes(), - )?; + if self.password_dir.is_none() && self.store_withdrawal_keystore { + return Err(Error::MissingPasswordDir); + } - // Write the withdrawal keystore to file. - write_keystore_to_file(dir.join(WITHDRAWAL_KEYSTORE_FILE), &withdrawal_keystore)?; + if let Some(password_dir) = self.password_dir.as_ref() { + // Only the withdrawal keystore if explicitly required. + if self.store_withdrawal_keystore { + // Write the withdrawal password to file. + write_password_to_file( + password_dir.join(withdrawal_keypair.pk.to_hex_string()), + withdrawal_password.as_bytes(), + )?; + + // Write the withdrawal keystore to file. + write_keystore_to_file( + dir.join(WITHDRAWAL_KEYSTORE_FILE), + &withdrawal_keystore, + )?; + } } } - // Write the voting password to file. - write_password_to_file( - self.password_dir - .join(format!("0x{}", voting_keystore.pubkey())), - voting_password.as_bytes(), - )?; + if let Some(password_dir) = self.password_dir.as_ref() { + // Write the voting password to file. + write_password_to_file( + password_dir.join(format!("0x{}", voting_keystore.pubkey())), + voting_password.as_bytes(), + )?; + } // Write the voting keystore to file. write_keystore_to_file(dir.join(VOTING_KEYSTORE_FILE), &voting_keystore)?; diff --git a/common/validator_dir/src/insecure_keys.rs b/common/validator_dir/src/insecure_keys.rs index 65bf036d1..8043db749 100644 --- a/common/validator_dir/src/insecure_keys.rs +++ b/common/validator_dir/src/insecure_keys.rs @@ -73,7 +73,8 @@ pub fn build_deterministic_validator_dirs( indices: &[usize], ) -> Result<(), String> { for &i in indices { - Builder::new(validators_dir.clone(), password_dir.clone()) + Builder::new(validators_dir.clone()) + .password_dir(password_dir.clone()) .insecure_voting_keypair(i) .map_err(|e| format!("Unable to generate insecure keypair: {:?}", e))? .store_withdrawal_keystore(false) diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index 23cb3a8c1..109566f66 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -129,6 +129,11 @@ impl ValidatorDir { &self.dir } + /// Returns the path to the voting keystore JSON file. + pub fn voting_keystore_path(&self) -> PathBuf { + self.dir.join(VOTING_KEYSTORE_FILE) + } + /// Attempts to read the keystore in `self.dir` and decrypt the keypair using a password file /// in `password_dir`. /// diff --git a/common/validator_dir/tests/tests.rs b/common/validator_dir/tests/tests.rs index 6e9bdc2b9..fd1b79f14 100644 --- a/common/validator_dir/tests/tests.rs +++ b/common/validator_dir/tests/tests.rs @@ -78,13 +78,11 @@ impl Harness { * Build the `ValidatorDir`. */ - let builder = Builder::new( - self.validators_dir.path().into(), - self.password_dir.path().into(), - ) - // Note: setting the withdrawal keystore here ensure that it can get overriden by later - // calls to `random_withdrawal_keystore`. - .store_withdrawal_keystore(config.store_withdrawal_keystore); + let builder = Builder::new(self.validators_dir.path().into()) + .password_dir(self.password_dir.path()) + // Note: setting the withdrawal keystore here ensure that it can get replaced by + // further calls to `random_withdrawal_keystore`. + .store_withdrawal_keystore(config.store_withdrawal_keystore); let builder = if config.random_voting_keystore { builder.random_voting_keystore().unwrap() @@ -208,13 +206,11 @@ fn without_voting_keystore() { let harness = Harness::new(); assert!(matches!( - Builder::new( - harness.validators_dir.path().into(), - harness.password_dir.path().into(), - ) - .random_withdrawal_keystore() - .unwrap() - .build(), + Builder::new(harness.validators_dir.path().into(),) + .password_dir(harness.password_dir.path()) + .random_withdrawal_keystore() + .unwrap() + .build(), Err(BuilderError::UninitializedVotingKeystore) )) } @@ -225,26 +221,22 @@ fn without_withdrawal_keystore() { let spec = &MainnetEthSpec::default_spec(); // Should build without withdrawal keystore if not storing the it or creating eth1 data. - Builder::new( - harness.validators_dir.path().into(), - harness.password_dir.path().into(), - ) - .random_voting_keystore() - .unwrap() - .store_withdrawal_keystore(false) - .build() - .unwrap(); + Builder::new(harness.validators_dir.path().into()) + .password_dir(harness.password_dir.path()) + .random_voting_keystore() + .unwrap() + .store_withdrawal_keystore(false) + .build() + .unwrap(); assert!( matches!( - Builder::new( - harness.validators_dir.path().into(), - harness.password_dir.path().into(), - ) - .random_voting_keystore() - .unwrap() - .store_withdrawal_keystore(true) - .build(), + Builder::new(harness.validators_dir.path().into(),) + .password_dir(harness.password_dir.path()) + .random_voting_keystore() + .unwrap() + .store_withdrawal_keystore(true) + .build(), Err(BuilderError::UninitializedWithdrawalKeystore) ), "storing the keystore requires keystore" @@ -252,14 +244,12 @@ fn without_withdrawal_keystore() { assert!( matches!( - Builder::new( - harness.validators_dir.path().into(), - harness.password_dir.path().into(), - ) - .random_voting_keystore() - .unwrap() - .create_eth1_tx_data(42, spec) - .build(), + Builder::new(harness.validators_dir.path().into(),) + .password_dir(harness.password_dir.path()) + .random_voting_keystore() + .unwrap() + .create_eth1_tx_data(42, spec) + .build(), Err(BuilderError::UninitializedWithdrawalKeystore) ), "storing the keystore requires keystore" diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index 98ddab5d8..5d4a0fbbc 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -13,3 +13,5 @@ types = { path = "../../consensus/types" } beacon_chain = { path = "../../beacon_node/beacon_chain" } state_processing = { path = "../../consensus/state_processing" } safe_arith = { path = "../../consensus/safe_arith" } +serde = { version = "1.0.110", features = ["derive"] } +tokio = { version = "0.2.21", features = ["sync"] } diff --git a/common/warp_utils/src/lib.rs b/common/warp_utils/src/lib.rs index ec9cf3c34..ba02273e6 100644 --- a/common/warp_utils/src/lib.rs +++ b/common/warp_utils/src/lib.rs @@ -3,3 +3,4 @@ pub mod reject; pub mod reply; +pub mod task; diff --git a/common/warp_utils/src/reject.rs b/common/warp_utils/src/reject.rs index 1243d5f68..020fa19d8 100644 --- a/common/warp_utils/src/reject.rs +++ b/common/warp_utils/src/reject.rs @@ -101,6 +101,15 @@ pub fn not_synced(msg: String) -> warp::reject::Rejection { warp::reject::custom(NotSynced(msg)) } +#[derive(Debug)] +pub struct InvalidAuthorization(pub String); + +impl Reject for InvalidAuthorization {} + +pub fn invalid_auth(msg: String) -> warp::reject::Rejection { + warp::reject::custom(InvalidAuthorization(msg)) +} + /// This function receives a `Rejection` and tries to return a custom /// value, otherwise simply passes the rejection along. pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply, Infallible> { @@ -150,6 +159,15 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply, } else if let Some(e) = err.find::<crate::reject::NotSynced>() { code = StatusCode::SERVICE_UNAVAILABLE; message = format!("SERVICE_UNAVAILABLE: beacon node is syncing: {}", e.0); + } else if let Some(e) = err.find::<crate::reject::InvalidAuthorization>() { + code = StatusCode::FORBIDDEN; + message = format!("FORBIDDEN: Invalid auth token: {}", e.0); + } else if let Some(e) = err.find::<warp::reject::MissingHeader>() { + code = StatusCode::BAD_REQUEST; + message = format!("BAD_REQUEST: missing {} header", e.name()); + } else if let Some(e) = err.find::<warp::reject::InvalidHeader>() { + code = StatusCode::BAD_REQUEST; + message = format!("BAD_REQUEST: invalid {} header", e.name()); } else if err.find::<warp::reject::MethodNotAllowed>().is_some() { code = StatusCode::METHOD_NOT_ALLOWED; message = "METHOD_NOT_ALLOWED".to_string(); diff --git a/common/warp_utils/src/task.rs b/common/warp_utils/src/task.rs new file mode 100644 index 000000000..da4cf91be --- /dev/null +++ b/common/warp_utils/src/task.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +/// Execute some task in a tokio "blocking thread". These threads are ideal for long-running +/// (blocking) tasks since they don't jam up the core executor. +pub async fn blocking_task<F, T>(func: F) -> T +where + F: Fn() -> T, +{ + tokio::task::block_in_place(func) +} + +/// A convenience wrapper around `blocking_task` for use with `warp` JSON responses. +pub async fn blocking_json_task<F, T>(func: F) -> Result<warp::reply::Json, warp::Rejection> +where + F: Fn() -> Result<T, warp::Rejection>, + T: Serialize, +{ + blocking_task(func) + .await + .map(|resp| warp::reply::json(&resp)) +} diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 6e3128b00..c1997680d 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -81,6 +81,7 @@ pub struct KeystoreBuilder<'a> { cipher: Cipher, uuid: Uuid, path: String, + description: String, } impl<'a> KeystoreBuilder<'a> { @@ -105,10 +106,17 @@ impl<'a> KeystoreBuilder<'a> { cipher: Cipher::Aes128Ctr(Aes128Ctr { iv }), uuid: Uuid::new_v4(), path, + description: "".to_string(), }) } } + /// Build the keystore with a specific description instead of an empty string. + pub fn description(mut self, description: String) -> Self { + self.description = description; + self + } + /// Build the keystore using the supplied `kdf` instead of `crate::default_kdf`. pub fn kdf(mut self, kdf: Kdf) -> Self { self.kdf = kdf; @@ -124,6 +132,7 @@ impl<'a> KeystoreBuilder<'a> { self.cipher, self.uuid, self.path, + self.description, ) } } @@ -147,6 +156,7 @@ impl Keystore { cipher: Cipher, uuid: Uuid, path: String, + description: String, ) -> Result<Self, Error> { let secret: ZeroizeHash = keypair.sk.serialize(); @@ -175,7 +185,7 @@ impl Keystore { path: Some(path), pubkey: keypair.pk.to_hex_string()[2..].to_string(), version: Version::four(), - description: None, + description: Some(description), name: None, }, }) @@ -228,6 +238,18 @@ impl Keystore { &self.json.pubkey } + /// Returns the description for the keystore, if the field is present. + pub fn description(&self) -> Option<&str> { + self.json.description.as_deref() + } + + /// Sets the description for the keystore. + /// + /// Note: this does not save the keystore to disk. + pub fn set_description(&mut self, description: String) { + self.json.description = Some(description) + } + /// Returns the pubkey for the keystore, parsed as a `PublicKey` if it parses. pub fn public_key(&self) -> Option<PublicKey> { serde_json::from_str(&format!("\"0x{}\"", &self.json.pubkey)).ok() diff --git a/crypto/eth2_wallet/src/wallet.rs b/crypto/eth2_wallet/src/wallet.rs index 47b2d329d..e0d0d04f6 100644 --- a/crypto/eth2_wallet/src/wallet.rs +++ b/crypto/eth2_wallet/src/wallet.rs @@ -215,6 +215,23 @@ impl Wallet { self.json.nextaccount } + /// Sets the value of the JSON wallet `nextaccount` field. + /// + /// This will be the index of the next wallet generated with `Self::next_validator`. + /// + /// ## Errors + /// + /// Returns `Err(())` if `nextaccount` is less than `self.nextaccount()` without mutating + /// `self`. This is to protect against duplicate validator generation. + pub fn set_nextaccount(&mut self, nextaccount: u32) -> Result<(), ()> { + if nextaccount >= self.nextaccount() { + self.json.nextaccount = nextaccount; + Ok(()) + } else { + Err(()) + } + } + /// Returns the value of the JSON wallet `name` field. pub fn name(&self) -> &str { &self.json.name diff --git a/lcli/src/insecure_validators.rs b/lcli/src/insecure_validators.rs index c04f854a5..2a604cffe 100644 --- a/lcli/src/insecure_validators.rs +++ b/lcli/src/insecure_validators.rs @@ -21,7 +21,8 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { for i in 0..validator_count { println!("Validator {}/{}", i + 1, validator_count); - ValidatorBuilder::new(validators_dir.clone(), secrets_dir.clone()) + ValidatorBuilder::new(validators_dir.clone()) + .password_dir(secrets_dir.clone()) .store_withdrawal_keystore(false) .insecure_voting_keypair(i) .map_err(|e| format!("Unable to generate keys: {:?}", e))? diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index ee191f38d..0d622990f 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" [dependencies] -tokio = { version = "0.2.21", features = ["macros"] } +tokio = { version = "0.2.21", features = ["full"] } slog = { version = "2.5.2", features = ["max_level_trace"] } sloggers = "1.0.0" types = { "path" = "../../consensus/types" } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 9d13706a1..376381c27 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -283,7 +283,7 @@ fn run<E: EthSpec>( let context = environment.core_context(); let log = context.log().clone(); let executor = context.executor.clone(); - let config = validator_client::Config::from_cli(&matches) + let config = validator_client::Config::from_cli(&matches, context.log()) .map_err(|e| format!("Unable to initialize validator config: {}", e))?; environment.runtime().spawn(async move { let run = async { diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 3c963f5b1..4ce7bb8ac 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -481,6 +481,7 @@ fn validator_import_launchpad() { let expected_def = ValidatorDefinition { enabled: true, + description: "".into(), voting_public_key: keystore.public_key().unwrap(), signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 0dd9b3424..ed17f69ee 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -128,7 +128,7 @@ impl<E: EthSpec> LocalNetwork<E> { .expect("Must have http started") }; - validator_config.http_server = + validator_config.beacon_node = format!("http://{}:{}", socket_addr.ip(), socket_addr.port()); let validator_client = LocalValidatorClient::production_with_insecure_keypairs( context, diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index b69b31f57..2f1b753e1 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -10,6 +10,8 @@ path = "src/lib.rs" [dev-dependencies] tokio = { version = "0.2.21", features = ["time", "rt-threaded", "macros"] } +tempfile = "3.1.0" +deposit_contract = { path = "../common/deposit_contract" } [dependencies] eth2_ssz = "0.1.2" @@ -47,3 +49,11 @@ validator_dir = { path = "../common/validator_dir" } clap_utils = { path = "../common/clap_utils" } eth2_keystore = { path = "../crypto/eth2_keystore" } account_utils = { path = "../common/account_utils" } +lighthouse_version = { path = "../common/lighthouse_version" } +warp_utils = { path = "../common/warp_utils" } +warp = "0.2.5" +hyper = "0.13.5" +serde_utils = { path = "../consensus/serde_utils" } +libsecp256k1 = "0.3.5" +ring = "0.16.12" +rand = "0.7.3" diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 9ad0c3faa..0651bf536 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -1,4 +1,4 @@ -use crate::config::DEFAULT_HTTP_SERVER; +use crate::config::DEFAULT_BEACON_NODE; use clap::{App, Arg}; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { @@ -8,13 +8,22 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { "When connected to a beacon node, performs the duties of a staked \ validator (e.g., proposing blocks and attestations).", ) + .arg( + Arg::with_name("beacon-node") + .long("beacon-node") + .value_name("NETWORK_ADDRESS") + .help("Address to a beacon node HTTP API") + .default_value(&DEFAULT_BEACON_NODE) + .takes_value(true), + ) + // This argument is deprecated, use `--beacon-node` instead. .arg( Arg::with_name("server") .long("server") .value_name("NETWORK_ADDRESS") - .help("Address to connect to BeaconNode.") - .default_value(&DEFAULT_HTTP_SERVER) - .takes_value(true), + .help("Deprecated. Use --beacon-node.") + .takes_value(true) + .conflicts_with("beacon-node"), ) .arg( Arg::with_name("validators-dir") @@ -97,4 +106,40 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("GRAFFITI") .takes_value(true) ) + /* REST API related arguments */ + .arg( + Arg::with_name("http") + .long("http") + .help("Enable the RESTful HTTP API server. Disabled by default.") + .takes_value(false), + ) + /* + * Note: there is purposefully no `--http-address` flag provided. + * + * The HTTP server is **not** encrypted (i.e., not HTTPS) and therefore it is unsafe to + * publish on a public network. + * + * We restrict the user to `127.0.0.1` and they must provide some other transport-layer + * encryption (e.g., SSH tunnels). + */ + .arg( + Arg::with_name("http-port") + .long("http-port") + .value_name("PORT") + .help("Set the listen TCP port for the RESTful HTTP API server. This server does **not** \ + provide encryption and is completely unsuitable to expose to a public network. \ + We do not provide a --http-address flag and restrict the user to listening on \ + 127.0.0.1. For access via the Internet, apply a transport-layer security like \ + a HTTPS reverse-proxy or SSH tunnelling.") + .default_value("5062") + .takes_value(true), + ) + .arg( + Arg::with_name("http-allow-origin") + .long("http-allow-origin") + .value_name("ORIGIN") + .help("Set the value of the Access-Control-Allow-Origin response HTTP header. Use * to allow any origin (not recommended in production)") + .default_value("") + .takes_value(true), + ) } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 1551f1aee..d51f44e44 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,3 +1,4 @@ +use crate::http_api; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use directory::{ @@ -6,10 +7,12 @@ use directory::{ }; use eth2::types::Graffiti; use serde_derive::{Deserialize, Serialize}; +use slog::{warn, Logger}; +use std::fs; use std::path::PathBuf; use types::GRAFFITI_BYTES_LEN; -pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/"; +pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; /// Stores the core configuration for this validator instance. #[derive(Clone, Serialize, Deserialize)] @@ -21,7 +24,7 @@ pub struct Config { /// The http endpoint of the beacon node API. /// /// Should be similar to `http://localhost:8080` - pub http_server: String, + pub beacon_node: String, /// If true, the validator client will still poll for duties and produce blocks even if the /// beacon node is not synced at startup. pub allow_unsynced_beacon_node: bool, @@ -33,6 +36,8 @@ pub struct Config { pub strict_slashing_protection: bool, /// Graffiti to be inserted everytime we create a block. pub graffiti: Option<Graffiti>, + /// Configuration for the HTTP REST API. + pub http_api: http_api::Config, } impl Default for Config { @@ -49,12 +54,13 @@ impl Default for Config { Self { validator_dir, secrets_dir, - http_server: DEFAULT_HTTP_SERVER.to_string(), + beacon_node: DEFAULT_BEACON_NODE.to_string(), allow_unsynced_beacon_node: false, delete_lockfiles: false, disable_auto_discover: false, strict_slashing_protection: false, graffiti: None, + http_api: <_>::default(), } } } @@ -62,7 +68,7 @@ impl Default for Config { impl Config { /// Returns a `Default` implementation of `Self` with some parameters modified by the supplied /// `cli_args`. - pub fn from_cli(cli_args: &ArgMatches) -> Result<Config, String> { + pub fn from_cli(cli_args: &ArgMatches, log: &Logger) -> Result<Config, String> { let mut config = Config::default(); let default_root_dir = dirs::home_dir() @@ -95,14 +101,22 @@ impl Config { }); if !config.validator_dir.exists() { - return Err(format!( - "The directory for validator data does not exist: {:?}", - config.validator_dir - )); + fs::create_dir_all(&config.validator_dir) + .map_err(|e| format!("Failed to create {:?}: {:?}", config.validator_dir, e))?; } + if let Some(beacon_node) = parse_optional(cli_args, "beacon-node")? { + config.beacon_node = beacon_node; + } + + // To be deprecated. if let Some(server) = parse_optional(cli_args, "server")? { - config.http_server = server; + warn!( + log, + "The --server flag is deprecated"; + "msg" => "please use --beacon-node instead" + ); + config.beacon_node = server; } config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced"); @@ -129,6 +143,29 @@ impl Config { } } + /* + * Http API server + */ + + if cli_args.is_present("http") { + config.http_api.enabled = true; + } + + if let Some(port) = cli_args.value_of("http-port") { + config.http_api.listen_port = port + .parse::<u16>() + .map_err(|_| "http-port is not a valid u16.")?; + } + + if let Some(allow_origin) = cli_args.value_of("http-allow-origin") { + // Pre-validate the config value to give feedback to the user on node startup, instead of + // as late as when the first API response is produced. + hyper::header::HeaderValue::from_str(allow_origin) + .map_err(|_| "Invalid allow-origin value")?; + + config.http_api.allow_origin = Some(allow_origin.to_string()); + } + Ok(config) } } diff --git a/validator_client/src/fork_service.rs b/validator_client/src/fork_service.rs index e38a4cf3c..58665ee01 100644 --- a/validator_client/src/fork_service.rs +++ b/validator_client/src/fork_service.rs @@ -2,31 +2,32 @@ use environment::RuntimeContext; use eth2::{types::StateId, BeaconNodeHttpClient}; use futures::StreamExt; use parking_lot::RwLock; +use slog::Logger; use slog::{debug, trace}; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; use tokio::time::{interval_at, Duration, Instant}; -use types::{ChainSpec, EthSpec, Fork}; +use types::{EthSpec, Fork}; /// Delay this period of time after the slot starts. This allows the node to process the new slot. const TIME_DELAY_FROM_SLOT: Duration = Duration::from_millis(80); /// Builds a `ForkService`. -pub struct ForkServiceBuilder<T, E: EthSpec> { +pub struct ForkServiceBuilder<T> { fork: Option<Fork>, slot_clock: Option<T>, beacon_node: Option<BeaconNodeHttpClient>, - context: Option<RuntimeContext<E>>, + log: Option<Logger>, } -impl<T: SlotClock + 'static, E: EthSpec> ForkServiceBuilder<T, E> { +impl<T: SlotClock + 'static> ForkServiceBuilder<T> { pub fn new() -> Self { Self { fork: None, slot_clock: None, beacon_node: None, - context: None, + log: None, } } @@ -40,12 +41,12 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkServiceBuilder<T, E> { self } - pub fn runtime_context(mut self, context: RuntimeContext<E>) -> Self { - self.context = Some(context); + pub fn log(mut self, log: Logger) -> Self { + self.log = Some(log); self } - pub fn build(self) -> Result<ForkService<T, E>, String> { + pub fn build(self) -> Result<ForkService<T>, String> { Ok(ForkService { inner: Arc::new(Inner { fork: RwLock::new(self.fork), @@ -55,28 +56,48 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkServiceBuilder<T, E> { beacon_node: self .beacon_node .ok_or_else(|| "Cannot build ForkService without beacon_node")?, - context: self - .context - .ok_or_else(|| "Cannot build ForkService without runtime_context")?, + log: self + .log + .ok_or_else(|| "Cannot build ForkService without logger")? + .clone(), }), }) } } +#[cfg(test)] +#[allow(dead_code)] +impl ForkServiceBuilder<slot_clock::TestingSlotClock> { + pub fn testing_only(log: Logger) -> Self { + Self { + fork: Some(types::Fork::default()), + slot_clock: Some(slot_clock::TestingSlotClock::new( + types::Slot::new(0), + std::time::Duration::from_secs(42), + std::time::Duration::from_secs(42), + )), + beacon_node: Some(eth2::BeaconNodeHttpClient::new( + eth2::Url::parse("http://127.0.0.1").unwrap(), + )), + log: Some(log), + } + } +} + /// Helper to minimise `Arc` usage. -pub struct Inner<T, E: EthSpec> { +pub struct Inner<T> { fork: RwLock<Option<Fork>>, beacon_node: BeaconNodeHttpClient, - context: RuntimeContext<E>, + log: Logger, slot_clock: T, } /// Attempts to download the `Fork` struct from the beacon node at the start of each epoch. -pub struct ForkService<T, E: EthSpec> { - inner: Arc<Inner<T, E>>, +pub struct ForkService<T> { + inner: Arc<Inner<T>>, } -impl<T, E: EthSpec> Clone for ForkService<T, E> { +impl<T> Clone for ForkService<T> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -84,22 +105,27 @@ impl<T, E: EthSpec> Clone for ForkService<T, E> { } } -impl<T, E: EthSpec> Deref for ForkService<T, E> { - type Target = Inner<T, E>; +impl<T> Deref for ForkService<T> { + type Target = Inner<T>; fn deref(&self) -> &Self::Target { self.inner.deref() } } -impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { +impl<T: SlotClock + 'static> ForkService<T> { /// Returns the last fork downloaded from the beacon node, if any. pub fn fork(&self) -> Option<Fork> { *self.fork.read() } /// Starts the service that periodically polls for the `Fork`. - pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { + pub fn start_update_service<E: EthSpec>( + self, + context: &RuntimeContext<E>, + ) -> Result<(), String> { + let spec = &context.eth2_config.spec; + let duration_to_next_epoch = self .slot_clock .duration_to_next_epoch(E::slots_per_epoch()) @@ -115,13 +141,12 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { }; // Run an immediate update before starting the updater service. - self.inner - .context + context .executor .runtime_handle() .spawn(self.clone().do_update()); - let executor = self.inner.context.executor.clone(); + let executor = context.executor.clone(); let interval_fut = async move { while interval.next().await.is_some() { @@ -136,8 +161,6 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { /// Attempts to download the `Fork` from the server. async fn do_update(self) -> Result<(), ()> { - let log = self.context.log(); - let fork = self .inner .beacon_node @@ -145,14 +168,14 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { .await .map_err(|e| { trace!( - log, + self.log, "Fork update failed"; "error" => format!("Error retrieving fork: {:?}", e) ) })? .ok_or_else(|| { trace!( - log, + self.log, "Fork update failed"; "error" => "The beacon head fork is unknown" ) @@ -163,7 +186,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ForkService<T, E> { *(self.fork.write()) = Some(fork); } - debug!(log, "Fork update success"); + debug!(self.log, "Fork update success"); // Returning an error will stop the interval. This is not desired, a single failure // should not stop all future attempts. diff --git a/validator_client/src/http_api/api_secret.rs b/validator_client/src/http_api/api_secret.rs new file mode 100644 index 000000000..a3aa5f0b9 --- /dev/null +++ b/validator_client/src/http_api/api_secret.rs @@ -0,0 +1,184 @@ +use eth2::lighthouse_vc::{PK_LEN, SECRET_PREFIX as PK_PREFIX}; +use rand::thread_rng; +use ring::digest::{digest, SHA256}; +use secp256k1::{Message, PublicKey, SecretKey}; +use std::fs; +use std::path::Path; +use warp::Filter; + +/// The name of the file which stores the secret key. +/// +/// It is purposefully opaque to prevent users confusing it with the "secret" that they need to +/// share with API consumers (which is actually the public key). +pub const SK_FILENAME: &str = ".secp-sk"; + +/// Length of the raw secret key, in bytes. +pub const SK_LEN: usize = 32; + +/// The name of the file which stores the public key. +/// +/// For users, this public key is a "secret" that can be shared with API consumers to provide them +/// access to the API. We avoid calling it a "public" key to users, since they should not post this +/// value in a public forum. +pub const PK_FILENAME: &str = "api-token.txt"; + +/// Contains a `secp256k1` keypair that is saved-to/loaded-from disk on instantiation. The keypair +/// is used for authorization/authentication for requests/responses on the HTTP API. +/// +/// Provides convenience functions to ultimately provide: +/// +/// - A signature across outgoing HTTP responses, applied to the `Signature` header. +/// - Verification of proof-of-knowledge of the public key in `self` for incoming HTTP requests, +/// via the `Authorization` header. +/// +/// The aforementioned scheme was first defined here: +/// +/// https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855 +pub struct ApiSecret { + pk: PublicKey, + sk: SecretKey, +} + +impl ApiSecret { + /// If both the secret and public keys are already on-disk, parse them and ensure they're both + /// from the same keypair. + /// + /// The provided `dir` is a directory containing two files, `SK_FILENAME` and `PK_FILENAME`. + /// + /// If either the secret or public key files are missing on disk, create a new keypair and + /// write it to disk (over-writing any existing files). + pub fn create_or_open<P: AsRef<Path>>(dir: P) -> Result<Self, String> { + let sk_path = dir.as_ref().join(SK_FILENAME); + let pk_path = dir.as_ref().join(PK_FILENAME); + + if !(sk_path.exists() && pk_path.exists()) { + let sk = SecretKey::random(&mut thread_rng()); + let pk = PublicKey::from_secret_key(&sk); + + fs::write( + &sk_path, + serde_utils::hex::encode(&sk.serialize()).as_bytes(), + ) + .map_err(|e| e.to_string())?; + fs::write( + &pk_path, + format!( + "{}{}", + PK_PREFIX, + serde_utils::hex::encode(&pk.serialize_compressed()[..]) + ) + .as_bytes(), + ) + .map_err(|e| e.to_string())?; + } + + let sk = fs::read(&sk_path) + .map_err(|e| format!("cannot read {}: {}", SK_FILENAME, e)) + .and_then(|bytes| { + serde_utils::hex::decode(&String::from_utf8_lossy(&bytes)) + .map_err(|_| format!("{} should be 0x-prefixed hex", PK_FILENAME)) + }) + .and_then(|bytes| { + if bytes.len() == SK_LEN { + let mut array = [0; SK_LEN]; + array.copy_from_slice(&bytes); + SecretKey::parse(&array).map_err(|e| format!("invalid {}: {}", SK_FILENAME, e)) + } else { + Err(format!( + "{} expected {} bytes not {}", + SK_FILENAME, + SK_LEN, + bytes.len() + )) + } + })?; + + let pk = fs::read(&pk_path) + .map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e)) + .and_then(|bytes| { + let hex = + String::from_utf8(bytes).map_err(|_| format!("{} is not utf8", SK_FILENAME))?; + if hex.starts_with(PK_PREFIX) { + serde_utils::hex::decode(&hex[PK_PREFIX.len()..]) + .map_err(|_| format!("{} should be 0x-prefixed hex", SK_FILENAME)) + } else { + Err(format!("unable to parse {}", SK_FILENAME)) + } + }) + .and_then(|bytes| { + if bytes.len() == PK_LEN { + let mut array = [0; PK_LEN]; + array.copy_from_slice(&bytes); + PublicKey::parse_compressed(&array) + .map_err(|e| format!("invalid {}: {}", PK_FILENAME, e)) + } else { + Err(format!( + "{} expected {} bytes not {}", + PK_FILENAME, + PK_LEN, + bytes.len() + )) + } + })?; + + // Ensure that the keys loaded from disk are indeed a pair. + if PublicKey::from_secret_key(&sk) != pk { + fs::remove_file(&sk_path) + .map_err(|e| format!("unable to remove {}: {}", SK_FILENAME, e))?; + fs::remove_file(&pk_path) + .map_err(|e| format!("unable to remove {}: {}", PK_FILENAME, e))?; + return Err(format!( + "{:?} does not match {:?} and the files have been deleted. Please try again.", + sk_path, pk_path + )); + } + + Ok(Self { sk, pk }) + } + + /// Returns the public key of `self` as a 0x-prefixed hex string. + fn pubkey_string(&self) -> String { + serde_utils::hex::encode(&self.pk.serialize_compressed()[..]) + } + + /// Returns the API token. + pub fn api_token(&self) -> String { + format!("{}{}", PK_PREFIX, self.pubkey_string()) + } + + /// Returns the value of the `Authorization` header which is used for verifying incoming HTTP + /// requests. + fn auth_header_value(&self) -> String { + format!("Basic {}", self.api_token()) + } + + /// Returns a `warp` header which filters out request that have a missing or inaccurate + /// `Authorization` header. + pub fn authorization_header_filter(&self) -> warp::filters::BoxedFilter<()> { + let expected = self.auth_header_value(); + warp::any() + .map(move || expected.clone()) + .and(warp::filters::header::header("Authorization")) + .and_then(move |expected: String, header: String| async move { + if header == expected { + Ok(()) + } else { + Err(warp_utils::reject::invalid_auth(header)) + } + }) + .untuple_one() + .boxed() + } + + /// Returns a closure which produces a signature over some bytes using the secret key in + /// `self`. The signature is a 32-byte hash formatted as a 0x-prefixed string. + pub fn signer(&self) -> impl Fn(&[u8]) -> String + Clone { + let sk = self.sk.clone(); + move |input: &[u8]| -> String { + let message = + Message::parse_slice(digest(&SHA256, input).as_ref()).expect("sha256 is 32 bytes"); + let (signature, _) = secp256k1::sign(&message, &sk); + serde_utils::hex::encode(signature.serialize_der().as_ref()) + } + } +} diff --git a/validator_client/src/http_api/create_validator.rs b/validator_client/src/http_api/create_validator.rs new file mode 100644 index 000000000..b84f9d614 --- /dev/null +++ b/validator_client/src/http_api/create_validator.rs @@ -0,0 +1,151 @@ +use crate::ValidatorStore; +use account_utils::{ + eth2_wallet::{bip39::Mnemonic, WalletBuilder}, + random_mnemonic, random_password, ZeroizeString, +}; +use eth2::lighthouse_vc::types::{self as api_types}; +use slot_clock::SlotClock; +use std::path::Path; +use types::ChainSpec; +use types::EthSpec; +use validator_dir::Builder as ValidatorDirBuilder; + +/// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in +/// this validator client. +/// +/// Returns the list of created validators and the mnemonic used to derive them via EIP-2334. +/// +/// ## Detail +/// +/// If `mnemonic_opt` is not supplied it will be randomly generated and returned in the response. +/// +/// If `key_derivation_path_offset` is supplied then the EIP-2334 validator index will start at +/// this point. +pub fn create_validators<P: AsRef<Path>, T: 'static + SlotClock, E: EthSpec>( + mnemonic_opt: Option<Mnemonic>, + key_derivation_path_offset: Option<u32>, + validator_requests: &[api_types::ValidatorRequest], + validator_dir: P, + validator_store: &ValidatorStore<T, E>, + spec: &ChainSpec, +) -> Result<(Vec<api_types::CreatedValidator>, Mnemonic), warp::Rejection> { + let mnemonic = mnemonic_opt.unwrap_or_else(random_mnemonic); + + let wallet_password = random_password(); + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_bytes(), String::new()) + .and_then(|builder| builder.build()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "unable to create EIP-2386 wallet: {:?}", + e + )) + })?; + + if let Some(nextaccount) = key_derivation_path_offset { + wallet.set_nextaccount(nextaccount).map_err(|()| { + warp_utils::reject::custom_server_error("unable to set wallet nextaccount".to_string()) + })?; + } + + let mut validators = Vec::with_capacity(validator_requests.len()); + + for request in validator_requests { + let voting_password = random_password(); + let withdrawal_password = random_password(); + let voting_password_string = ZeroizeString::from( + String::from_utf8(voting_password.as_bytes().to_vec()).map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "locally generated password is not utf8: {:?}", + e + )) + })?, + ); + + let mut keystores = wallet + .next_validator( + wallet_password.as_bytes(), + voting_password.as_bytes(), + withdrawal_password.as_bytes(), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "unable to create validator keys: {:?}", + e + )) + })?; + + keystores + .voting + .set_description(request.description.clone()); + keystores + .withdrawal + .set_description(request.description.clone()); + + let voting_pubkey = format!("0x{}", keystores.voting.pubkey()) + .parse() + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "created invalid public key: {:?}", + e + )) + })?; + + let validator_dir = ValidatorDirBuilder::new(validator_dir.as_ref().into()) + .voting_keystore(keystores.voting, voting_password.as_bytes()) + .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) + .create_eth1_tx_data(request.deposit_gwei, &spec) + .store_withdrawal_keystore(false) + .build() + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to build validator directory: {:?}", + e + )) + })?; + + let eth1_deposit_data = validator_dir + .eth1_deposit_data() + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to read local deposit data: {:?}", + e + )) + })? + .ok_or_else(|| { + warp_utils::reject::custom_server_error( + "failed to create local deposit data: {:?}".to_string(), + ) + })?; + + if eth1_deposit_data.deposit_data.amount != request.deposit_gwei { + return Err(warp_utils::reject::custom_server_error(format!( + "invalid deposit_gwei {}, expected {}", + eth1_deposit_data.deposit_data.amount, request.deposit_gwei + ))); + } + + tokio::runtime::Handle::current() + .block_on(validator_store.add_validator_keystore( + validator_dir.voting_keystore_path(), + voting_password_string, + request.enable, + )) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to initialize validator: {:?}", + e + )) + })?; + + validators.push(api_types::CreatedValidator { + enabled: request.enable, + description: request.description.clone(), + voting_pubkey, + eth1_deposit_tx_data: serde_utils::hex::encode(ð1_deposit_data.rlp), + deposit_gwei: request.deposit_gwei, + }); + } + + Ok((validators, mnemonic)) +} diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs new file mode 100644 index 000000000..7e0d387d2 --- /dev/null +++ b/validator_client/src/http_api/mod.rs @@ -0,0 +1,488 @@ +mod api_secret; +mod create_validator; +mod tests; + +use crate::ValidatorStore; +use account_utils::mnemonic_from_phrase; +use create_validator::create_validators; +use eth2::lighthouse_vc::types::{self as api_types, PublicKey, PublicKeyBytes}; +use lighthouse_version::version_with_platform; +use serde::{Deserialize, Serialize}; +use slog::{crit, info, Logger}; +use slot_clock::SlotClock; +use std::future::Future; +use std::marker::PhantomData; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::path::PathBuf; +use std::sync::Arc; +use types::{ChainSpec, EthSpec, YamlConfig}; +use validator_dir::Builder as ValidatorDirBuilder; +use warp::{ + http::{ + header::{HeaderValue, CONTENT_TYPE}, + response::Response, + StatusCode, + }, + Filter, +}; + +pub use api_secret::ApiSecret; + +#[derive(Debug)] +pub enum Error { + Warp(warp::Error), + Other(String), +} + +impl From<warp::Error> for Error { + fn from(e: warp::Error) -> Self { + Error::Warp(e) + } +} + +impl From<String> for Error { + fn from(e: String) -> Self { + Error::Other(e) + } +} + +/// A wrapper around all the items required to spawn the HTTP server. +/// +/// The server will gracefully handle the case where any fields are `None`. +pub struct Context<T: Clone, E: EthSpec> { + pub api_secret: ApiSecret, + pub validator_store: Option<ValidatorStore<T, E>>, + pub validator_dir: Option<PathBuf>, + pub spec: ChainSpec, + pub config: Config, + pub log: Logger, + pub _phantom: PhantomData<E>, +} + +/// Configuration for the HTTP server. +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub enabled: bool, + pub listen_addr: Ipv4Addr, + pub listen_port: u16, + pub allow_origin: Option<String>, +} + +impl Default for Config { + fn default() -> Self { + Self { + enabled: false, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 5062, + allow_origin: None, + } + } +} + +/// Creates a server that will serve requests using information from `ctx`. +/// +/// The server will shut down gracefully when the `shutdown` future resolves. +/// +/// ## Returns +/// +/// This function will bind the server to the provided address and then return a tuple of: +/// +/// - `SocketAddr`: the address that the HTTP server will listen on. +/// - `Future`: the actual server future that will need to be awaited. +/// +/// ## Errors +/// +/// Returns an error if the server is unable to bind or there is another error during +/// configuration. +pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( + ctx: Arc<Context<T, E>>, + shutdown: impl Future<Output = ()> + Send + Sync + 'static, +) -> Result<(SocketAddr, impl Future<Output = ()>), Error> { + let config = &ctx.config; + let log = ctx.log.clone(); + let allow_origin = config.allow_origin.clone(); + + // Sanity check. + if !config.enabled { + crit!(log, "Cannot start disabled metrics HTTP server"); + return Err(Error::Other( + "A disabled metrics server should not be started".to_string(), + )); + } + + let authorization_header_filter = ctx.api_secret.authorization_header_filter(); + let api_token = ctx.api_secret.api_token(); + let signer = ctx.api_secret.signer(); + let signer = warp::any().map(move || signer.clone()); + + let inner_validator_store = ctx.validator_store.clone(); + let validator_store_filter = warp::any() + .map(move || inner_validator_store.clone()) + .and_then(|validator_store: Option<_>| async move { + validator_store.ok_or_else(|| { + warp_utils::reject::custom_not_found( + "validator store is not initialized.".to_string(), + ) + }) + }); + + let inner_validator_dir = ctx.validator_dir.clone(); + let validator_dir_filter = warp::any() + .map(move || inner_validator_dir.clone()) + .and_then(|validator_dir: Option<_>| async move { + validator_dir.ok_or_else(|| { + warp_utils::reject::custom_not_found( + "validator_dir directory is not initialized.".to_string(), + ) + }) + }); + + let inner_spec = Arc::new(ctx.spec.clone()); + let spec_filter = warp::any().map(move || inner_spec.clone()); + + // GET lighthouse/version + let get_node_version = warp::path("lighthouse") + .and(warp::path("version")) + .and(warp::path::end()) + .and(signer.clone()) + .and_then(|signer| { + blocking_signed_json_task(signer, move || { + Ok(api_types::GenericResponse::from(api_types::VersionData { + version: version_with_platform(), + })) + }) + }); + + // GET lighthouse/health + let get_lighthouse_health = warp::path("lighthouse") + .and(warp::path("health")) + .and(warp::path::end()) + .and(signer.clone()) + .and_then(|signer| { + blocking_signed_json_task(signer, move || { + eth2::lighthouse::Health::observe() + .map(api_types::GenericResponse::from) + .map_err(warp_utils::reject::custom_bad_request) + }) + }); + + // GET lighthouse/spec + let get_lighthouse_spec = warp::path("lighthouse") + .and(warp::path("spec")) + .and(warp::path::end()) + .and(spec_filter.clone()) + .and(signer.clone()) + .and_then(|spec: Arc<_>, signer| { + blocking_signed_json_task(signer, move || { + Ok(api_types::GenericResponse::from( + YamlConfig::from_spec::<E>(&spec), + )) + }) + }); + + // GET lighthouse/validators + let get_lighthouse_validators = warp::path("lighthouse") + .and(warp::path("validators")) + .and(warp::path::end()) + .and(validator_store_filter.clone()) + .and(signer.clone()) + .and_then(|validator_store: ValidatorStore<T, E>, signer| { + blocking_signed_json_task(signer, move || { + let validators = validator_store + .initialized_validators() + .read() + .validator_definitions() + .iter() + .map(|def| api_types::ValidatorData { + enabled: def.enabled, + description: def.description.clone(), + voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), + }) + .collect::<Vec<_>>(); + + Ok(api_types::GenericResponse::from(validators)) + }) + }); + + // GET lighthouse/validators/{validator_pubkey} + let get_lighthouse_validators_pubkey = warp::path("lighthouse") + .and(warp::path("validators")) + .and(warp::path::param::<PublicKey>()) + .and(warp::path::end()) + .and(validator_store_filter.clone()) + .and(signer.clone()) + .and_then( + |validator_pubkey: PublicKey, validator_store: ValidatorStore<T, E>, signer| { + blocking_signed_json_task(signer, move || { + let validator = validator_store + .initialized_validators() + .read() + .validator_definitions() + .iter() + .find(|def| def.voting_public_key == validator_pubkey) + .map(|def| api_types::ValidatorData { + enabled: def.enabled, + description: def.description.clone(), + voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), + }) + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "no validator for {:?}", + validator_pubkey + )) + })?; + + Ok(api_types::GenericResponse::from(validator)) + }) + }, + ); + + // POST lighthouse/validators/ + let post_validators = warp::path("lighthouse") + .and(warp::path("validators")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(validator_dir_filter.clone()) + .and(validator_store_filter.clone()) + .and(spec_filter.clone()) + .and(signer.clone()) + .and_then( + |body: Vec<api_types::ValidatorRequest>, + validator_dir: PathBuf, + validator_store: ValidatorStore<T, E>, + spec: Arc<ChainSpec>, + signer| { + blocking_signed_json_task(signer, move || { + let (validators, mnemonic) = create_validators( + None, + None, + &body, + &validator_dir, + &validator_store, + &spec, + )?; + let response = api_types::PostValidatorsResponseData { + mnemonic: mnemonic.into_phrase().into(), + validators, + }; + Ok(api_types::GenericResponse::from(response)) + }) + }, + ); + + // POST lighthouse/validators/mnemonic + let post_validators_mnemonic = warp::path("lighthouse") + .and(warp::path("validators")) + .and(warp::path("mnemonic")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(validator_dir_filter.clone()) + .and(validator_store_filter.clone()) + .and(spec_filter) + .and(signer.clone()) + .and_then( + |body: api_types::CreateValidatorsMnemonicRequest, + validator_dir: PathBuf, + validator_store: ValidatorStore<T, E>, + spec: Arc<ChainSpec>, + signer| { + blocking_signed_json_task(signer, move || { + let mnemonic = mnemonic_from_phrase(body.mnemonic.as_str()).map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid mnemonic: {:?}", e)) + })?; + let (validators, _mnemonic) = create_validators( + Some(mnemonic), + Some(body.key_derivation_path_offset), + &body.validators, + &validator_dir, + &validator_store, + &spec, + )?; + Ok(api_types::GenericResponse::from(validators)) + }) + }, + ); + + // POST lighthouse/validators/keystore + let post_validators_keystore = warp::path("lighthouse") + .and(warp::path("validators")) + .and(warp::path("keystore")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(validator_dir_filter) + .and(validator_store_filter.clone()) + .and(signer.clone()) + .and_then( + |body: api_types::KeystoreValidatorsPostRequest, + validator_dir: PathBuf, + validator_store: ValidatorStore<T, E>, + signer| { + blocking_signed_json_task(signer, move || { + // Check to ensure the password is correct. + let keypair = body + .keystore + .decrypt_keypair(body.password.as_ref()) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "invalid keystore: {:?}", + e + )) + })?; + + let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) + .voting_keystore(body.keystore.clone(), body.password.as_ref()) + .store_withdrawal_keystore(false) + .build() + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to build validator directory: {:?}", + e + )) + })?; + + let voting_password = body.password.clone(); + + let validator_def = tokio::runtime::Handle::current() + .block_on(validator_store.add_validator_keystore( + validator_dir.voting_keystore_path(), + voting_password, + body.enable, + )) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to initialize validator: {:?}", + e + )) + })?; + + Ok(api_types::GenericResponse::from(api_types::ValidatorData { + enabled: body.enable, + description: validator_def.description, + voting_pubkey: keypair.pk.into(), + })) + }) + }, + ); + + // PATCH lighthouse/validators/{validator_pubkey} + let patch_validators = warp::path("lighthouse") + .and(warp::path("validators")) + .and(warp::path::param::<PublicKey>()) + .and(warp::path::end()) + .and(warp::body::json()) + .and(validator_store_filter) + .and(signer) + .and_then( + |validator_pubkey: PublicKey, + body: api_types::ValidatorPatchRequest, + validator_store: ValidatorStore<T, E>, + signer| { + blocking_signed_json_task(signer, move || { + let initialized_validators_rw_lock = validator_store.initialized_validators(); + let mut initialized_validators = initialized_validators_rw_lock.write(); + + match initialized_validators.is_enabled(&validator_pubkey) { + None => Err(warp_utils::reject::custom_not_found(format!( + "no validator for {:?}", + validator_pubkey + ))), + Some(enabled) if enabled == body.enabled => Ok(()), + Some(_) => { + tokio::runtime::Handle::current() + .block_on( + initialized_validators + .set_validator_status(&validator_pubkey, body.enabled), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "unable to set validator status: {:?}", + e + )) + })?; + + Ok(()) + } + } + }) + }, + ); + + let routes = warp::any() + .and(authorization_header_filter) + .and( + warp::get().and( + get_node_version + .or(get_lighthouse_health) + .or(get_lighthouse_spec) + .or(get_lighthouse_validators) + .or(get_lighthouse_validators_pubkey), + ), + ) + .or(warp::post().and( + post_validators + .or(post_validators_keystore) + .or(post_validators_mnemonic), + )) + .or(warp::patch().and(patch_validators)) + // Maps errors into HTTP responses. + .recover(warp_utils::reject::handle_rejection) + // Add a `Server` header. + .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) + // Maybe add some CORS headers. + .map(move |reply| warp_utils::reply::maybe_cors(reply, allow_origin.as_ref())); + + let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( + SocketAddrV4::new(config.listen_addr, config.listen_port), + async { + shutdown.await; + }, + )?; + + info!( + log, + "HTTP API started"; + "listen_address" => listening_socket.to_string(), + "api_token" => api_token, + ); + + Ok((listening_socket, server)) +} + +/// Executes `func` in blocking tokio task (i.e., where long-running tasks are permitted). +/// JSON-encodes the return value of `func`, using the `signer` function to produce a signature of +/// those bytes. +pub async fn blocking_signed_json_task<S, F, T>( + signer: S, + func: F, +) -> Result<impl warp::Reply, warp::Rejection> +where + S: Fn(&[u8]) -> String, + F: Fn() -> Result<T, warp::Rejection>, + T: Serialize, +{ + warp_utils::task::blocking_task(func) + .await + .map(|func_output| { + let mut response = match serde_json::to_vec(&func_output) { + Ok(body) => { + let mut res = Response::new(body); + res.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + res + } + Err(_) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(vec![]) + .expect("can produce simple response from static values"), + }; + + let body: &Vec<u8> = response.body(); + let signature = signer(body); + let header_value = + HeaderValue::from_str(&signature).expect("hash can be encoded as header"); + + response.headers_mut().append("Signature", header_value); + + response + }) +} diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs new file mode 100644 index 000000000..e9344b5f4 --- /dev/null +++ b/validator_client/src/http_api/tests.rs @@ -0,0 +1,527 @@ +#![cfg(test)] +#![cfg(not(debug_assertions))] + +use crate::{ + http_api::{ApiSecret, Config as HttpConfig, Context}, + Config, ForkServiceBuilder, InitializedValidators, ValidatorDefinitions, ValidatorStore, +}; +use account_utils::{ + eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, + ZeroizeString, +}; +use deposit_contract::decode_eth1_tx_data; +use environment::null_logger; +use eth2::{ + lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, + Url, +}; +use eth2_keystore::KeystoreBuilder; +use parking_lot::RwLock; +use slot_clock::TestingSlotClock; +use std::marker::PhantomData; +use std::net::Ipv4Addr; +use std::sync::Arc; +use tempfile::{tempdir, TempDir}; +use tokio::sync::oneshot; + +const PASSWORD_BYTES: &[u8] = &[42, 13, 37]; + +type E = MainnetEthSpec; + +struct ApiTester { + client: ValidatorClientHttpClient, + initialized_validators: Arc<RwLock<InitializedValidators>>, + url: Url, + _server_shutdown: oneshot::Sender<()>, + _validator_dir: TempDir, +} + +impl ApiTester { + pub async fn new() -> Self { + let log = null_logger().unwrap(); + + let validator_dir = tempdir().unwrap(); + let secrets_dir = tempdir().unwrap(); + + let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); + + let initialized_validators = InitializedValidators::from_definitions( + validator_defs, + validator_dir.path().into(), + false, + log.clone(), + ) + .await + .unwrap(); + + let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_pubkey = api_secret.api_token(); + + let mut config = Config::default(); + config.validator_dir = validator_dir.path().into(); + config.secrets_dir = secrets_dir.path().into(); + + let fork_service = ForkServiceBuilder::testing_only(log.clone()) + .build() + .unwrap(); + + let validator_store: ValidatorStore<TestingSlotClock, E> = ValidatorStore::new( + initialized_validators, + &config, + Hash256::repeat_byte(42), + E::default_spec(), + fork_service.clone(), + log.clone(), + ) + .unwrap(); + + let initialized_validators = validator_store.initialized_validators(); + + let context: Arc<Context<TestingSlotClock, E>> = Arc::new(Context { + api_secret, + validator_dir: Some(validator_dir.path().into()), + validator_store: Some(validator_store), + spec: E::default_spec(), + config: HttpConfig { + enabled: true, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 0, + allow_origin: None, + }, + log, + _phantom: PhantomData, + }); + let ctx = context.clone(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_shutdown = async { + // It's not really interesting why this triggered, just that it happened. + let _ = shutdown_rx.await; + }; + let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); + + tokio::spawn(async { server.await }); + + let url = Url::parse(&format!( + "http://{}:{}", + listening_socket.ip(), + listening_socket.port() + )) + .unwrap(); + + let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey).unwrap(); + + Self { + initialized_validators, + _validator_dir: validator_dir, + client, + url, + _server_shutdown: shutdown_tx, + } + } + + pub fn invalidate_api_token(mut self) -> Self { + let tmp = tempdir().unwrap(); + let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let invalid_pubkey = api_secret.api_token(); + + self.client = ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey).unwrap(); + self + } + + pub async fn test_get_lighthouse_version_invalid(self) -> Self { + self.client.get_lighthouse_version().await.unwrap_err(); + self + } + + pub async fn test_get_lighthouse_spec(self) -> Self { + let result = self.client.get_lighthouse_spec().await.unwrap().data; + + let expected = YamlConfig::from_spec::<E>(&E::default_spec()); + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_lighthouse_version(self) -> Self { + let result = self.client.get_lighthouse_version().await.unwrap().data; + + let expected = VersionData { + version: lighthouse_version::version_with_platform(), + }; + + assert_eq!(result, expected); + + self + } + + #[cfg(target_os = "linux")] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap(); + + self + } + + #[cfg(not(target_os = "linux"))] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap_err(); + + self + } + pub fn vals_total(&self) -> usize { + self.initialized_validators.read().num_total() + } + + pub fn vals_enabled(&self) -> usize { + self.initialized_validators.read().num_enabled() + } + + pub fn assert_enabled_validators_count(self, count: usize) -> Self { + assert_eq!(self.vals_enabled(), count); + self + } + + pub fn assert_validators_count(self, count: usize) -> Self { + assert_eq!(self.vals_total(), count); + self + } + + pub async fn create_hd_validators(self, s: HdValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let validators = (0..s.count) + .map(|i| ValidatorRequest { + enable: !s.disabled.contains(&i), + description: format!("boi #{}", i), + deposit_gwei: E::default_spec().max_effective_balance, + }) + .collect::<Vec<_>>(); + + let (response, mnemonic) = if s.specify_mnemonic { + let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let request = CreateValidatorsMnemonicRequest { + mnemonic: mnemonic.clone(), + key_derivation_path_offset: s.key_derivation_path_offset, + validators: validators.clone(), + }; + let response = self + .client + .post_lighthouse_validators_mnemonic(&request) + .await + .unwrap() + .data; + + (response, mnemonic) + } else { + assert_eq!( + s.key_derivation_path_offset, 0, + "cannot use a derivation offset without specifying a mnemonic" + ); + let response = self + .client + .post_lighthouse_validators(validators.clone()) + .await + .unwrap() + .data; + (response.validators.clone(), response.mnemonic.clone()) + }; + + assert_eq!(response.len(), s.count); + assert_eq!(self.vals_total(), initial_vals + s.count); + assert_eq!( + self.vals_enabled(), + initial_enabled_vals + s.count - s.disabled.len() + ); + + let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; + + assert_eq!(server_vals.len(), self.vals_total()); + + // Ensure the server lists all of these newly created validators. + for validator in &response { + assert!(server_vals + .iter() + .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey)); + } + + /* + * Verify that we can regenerate all the keys from the mnemonic. + */ + + let mnemonic = mnemonic_from_phrase(mnemonic.as_str()).unwrap(); + let mut wallet = WalletBuilder::from_mnemonic(&mnemonic, PASSWORD_BYTES, "".to_string()) + .unwrap() + .build() + .unwrap(); + + wallet + .set_nextaccount(s.key_derivation_path_offset) + .unwrap(); + + for i in 0..s.count { + let keypairs = wallet + .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) + .unwrap(); + let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); + + assert_eq!( + response[i].voting_pubkey, + voting_keypair.pk.clone().into(), + "the locally generated voting pk should match the server response" + ); + + let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); + + let deposit_bytes = + serde_utils::hex::decode(&response[i].eth1_deposit_tx_data).unwrap(); + + let (deposit_data, _) = + decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) + .unwrap(); + + assert_eq!( + deposit_data.pubkey, + voting_keypair.pk.clone().into(), + "the locally generated voting pk should match the deposit data" + ); + + assert_eq!( + deposit_data.withdrawal_credentials, + Hash256::from_slice(&bls::get_withdrawal_credentials( + &withdrawal_keypair.pk, + E::default_spec().bls_withdrawal_prefix_byte + )), + "the locally generated withdrawal creds should match the deposit data" + ); + + assert_eq!( + deposit_data.signature, + deposit_data.create_signature(&voting_keypair.sk, &E::default_spec()), + "the locally-generated deposit sig should create the same deposit sig" + ); + } + + self + } + + pub async fn create_keystore_validators(self, s: KeystoreValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let password = random_password(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) + .unwrap() + .build() + .unwrap(); + + if !s.correct_password { + let request = KeystoreValidatorsPostRequest { + enable: s.enabled, + password: String::from_utf8(random_password().as_ref().to_vec()) + .unwrap() + .into(), + keystore, + }; + + self.client + .post_lighthouse_validators_keystore(&request) + .await + .unwrap_err(); + + return self; + } + + let request = KeystoreValidatorsPostRequest { + enable: s.enabled, + password: String::from_utf8(password.as_ref().to_vec()) + .unwrap() + .into(), + keystore, + }; + + let response = self + .client + .post_lighthouse_validators_keystore(&request) + .await + .unwrap() + .data; + + let num_enabled = s.enabled as usize; + + assert_eq!(self.vals_total(), initial_vals + 1); + assert_eq!(self.vals_enabled(), initial_enabled_vals + num_enabled); + + let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; + + assert_eq!(server_vals.len(), self.vals_total()); + + assert_eq!(response.voting_pubkey, keypair.pk.into()); + assert_eq!(response.enabled, s.enabled); + + self + } + + pub async fn set_validator_enabled(self, index: usize, enabled: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators(&validator.voting_pubkey, enabled) + .await + .unwrap(); + + assert_eq!( + self.initialized_validators + .read() + .is_enabled(&validator.voting_pubkey.decompress().unwrap()) + .unwrap(), + enabled + ); + + assert!(self + .client + .get_lighthouse_validators() + .await + .unwrap() + .data + .into_iter() + .find(|v| v.voting_pubkey == validator.voting_pubkey) + .map(|v| v.enabled == enabled) + .unwrap()); + + // Check the server via an individual request. + assert_eq!( + self.client + .get_lighthouse_validators_pubkey(&validator.voting_pubkey) + .await + .unwrap() + .unwrap() + .data + .enabled, + enabled + ); + + self + } +} + +struct HdValidatorScenario { + count: usize, + specify_mnemonic: bool, + key_derivation_path_offset: u32, + disabled: Vec<usize>, +} + +struct KeystoreValidatorScenario { + enabled: bool, + correct_password: bool, +} + +#[tokio::test(core_threads = 2)] +async fn invalid_pubkey() { + ApiTester::new() + .await + .invalidate_api_token() + .test_get_lighthouse_version_invalid() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn simple_getters() { + ApiTester::new() + .await + .test_get_lighthouse_version() + .await + .test_get_lighthouse_health() + .await + .test_get_lighthouse_spec() + .await; +} + +#[tokio::test(core_threads = 2)] +async fn hd_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: true, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .create_hd_validators(HdValidatorScenario { + count: 1, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![0], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3) + .create_hd_validators(HdValidatorScenario { + count: 0, + specify_mnemonic: true, + key_derivation_path_offset: 4, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3); +} + +#[tokio::test(core_threads = 2)] +async fn validator_enabling() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2); +} + +#[tokio::test(core_threads = 2)] +async fn keystore_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: false, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: false, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2); +} diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index a097d7245..dbab008e8 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -56,6 +56,8 @@ pub enum Error { TokioJoin(tokio::task::JoinError), /// There was a filesystem error when deleting a lockfile. UnableToDeleteLockfile(io::Error), + /// Cannot initialize the same validator twice. + DuplicatePublicKey, } /// A method used by a validator to sign messages. @@ -322,6 +324,42 @@ impl InitializedValidators { .map(|v| v.voting_keypair()) } + /// Add a validator definition to `self`, overwriting the on-disk representation of `self`. + pub async fn add_definition(&mut self, def: ValidatorDefinition) -> Result<(), Error> { + if self + .definitions + .as_slice() + .iter() + .any(|existing| existing.voting_public_key == def.voting_public_key) + { + return Err(Error::DuplicatePublicKey); + } + + self.definitions.push(def); + + self.update_validators().await?; + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(()) + } + + /// Returns a slice of all defined validators (regardless of their enabled state). + pub fn validator_definitions(&self) -> &[ValidatorDefinition] { + self.definitions.as_slice() + } + + /// Indicates if the `voting_public_key` exists in self and if it is enabled. + pub fn is_enabled(&self, voting_public_key: &PublicKey) -> Option<bool> { + self.definitions + .as_slice() + .iter() + .find(|def| def.voting_public_key == *voting_public_key) + .map(|def| def.enabled) + } + /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values. /// /// ## Notes diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 8a0e8ba1e..034271199 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -10,6 +10,8 @@ mod notifier; mod validator_duty; mod validator_store; +pub mod http_api; + pub use cli::cli_app; pub use config::Config; @@ -22,11 +24,14 @@ use environment::RuntimeContext; use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Url}; use fork_service::{ForkService, ForkServiceBuilder}; use futures::channel::mpsc; +use http_api::ApiSecret; use initialized_validators::InitializedValidators; use notifier::spawn_notifier; use slog::{error, info, Logger}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; +use std::marker::PhantomData; +use std::net::SocketAddr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{delay_for, Duration}; @@ -42,9 +47,11 @@ const HTTP_TIMEOUT: Duration = Duration::from_secs(12); pub struct ProductionValidatorClient<T: EthSpec> { context: RuntimeContext<T>, duties_service: DutiesService<SystemTimeSlotClock, T>, - fork_service: ForkService<SystemTimeSlotClock, T>, + fork_service: ForkService<SystemTimeSlotClock>, block_service: BlockService<SystemTimeSlotClock, T>, attestation_service: AttestationService<SystemTimeSlotClock, T>, + validator_store: ValidatorStore<SystemTimeSlotClock, T>, + http_api_listen_addr: Option<SocketAddr>, config: Config, } @@ -55,7 +62,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { context: RuntimeContext<T>, cli_args: &ArgMatches<'_>, ) -> Result<Self, String> { - let config = Config::from_cli(&cli_args) + let config = Config::from_cli(&cli_args, context.log()) .map_err(|e| format!("Unable to initialize config: {}", e))?; Self::new(context, config).await } @@ -68,7 +75,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { info!( log, "Starting validator client"; - "beacon_node" => &config.http_server, + "beacon_node" => &config.beacon_node, "validator_dir" => format!("{:?}", config.validator_dir), ); @@ -106,7 +113,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { ); let beacon_node_url: Url = config - .http_server + .beacon_node .parse() .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; let beacon_node_http_client = ClientBuilder::new() @@ -144,7 +151,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { let fork_service = ForkServiceBuilder::new() .slot_clock(slot_clock.clone()) .beacon_node(beacon_node.clone()) - .runtime_context(context.service_context("fork".into())) + .log(log.clone()) .build()?; let validator_store: ValidatorStore<SystemTimeSlotClock, T> = ValidatorStore::new( @@ -183,7 +190,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { let attestation_service = AttestationServiceBuilder::new() .duties_service(duties_service.clone()) .slot_clock(slot_clock) - .validator_store(validator_store) + .validator_store(validator_store.clone()) .beacon_node(beacon_node) .runtime_context(context.service_context("attestation".into())) .build()?; @@ -194,7 +201,9 @@ impl<T: EthSpec> ProductionValidatorClient<T> { fork_service, block_service, attestation_service, + validator_store, config, + http_api_listen_addr: None, }) } @@ -204,6 +213,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { // whole epoch! let channel_capacity = T::slots_per_epoch() as usize; let (block_service_tx, block_service_rx) = mpsc::channel(channel_capacity); + let log = self.context.log(); self.duties_service .clone() @@ -215,7 +225,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { self.fork_service .clone() - .start_update_service(&self.context.eth2_config.spec) + .start_update_service(&self.context) .map_err(|e| format!("Unable to start fork service: {}", e))?; self.block_service @@ -230,6 +240,35 @@ impl<T: EthSpec> ProductionValidatorClient<T> { spawn_notifier(self).map_err(|e| format!("Failed to start notifier: {}", e))?; + let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; + + self.http_api_listen_addr = if self.config.http_api.enabled { + let ctx: Arc<http_api::Context<SystemTimeSlotClock, T>> = Arc::new(http_api::Context { + api_secret, + validator_store: Some(self.validator_store.clone()), + validator_dir: Some(self.config.validator_dir.clone()), + spec: self.context.eth2_config.spec.clone(), + config: self.config.http_api.clone(), + log: log.clone(), + _phantom: PhantomData, + }); + + let exit = self.context.executor.exit(); + + let (listen_addr, server) = http_api::serve(ctx, exit) + .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; + + self.context + .clone() + .executor + .spawn_without_exit(async move { server.await }, "http-api"); + + Some(listen_addr) + } else { + info!(log, "HTTP API server is disabled"); + None + }; + Ok(()) } } diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index d9ee7faec..c997979b9 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -45,7 +45,12 @@ pub fn spawn_notifier<T: EthSpec>(client: &ProductionValidatorClient<T>) -> Resu let attesting_validators = duties_service.attester_count(epoch); if total_validators == 0 { - error!(log, "No validators present") + info!( + log, + "No validators present"; + "msg" => "see `lighthouse account validator create --help` \ + or the HTTP API documentation" + ) } else if total_validators == attesting_validators { info!( log, diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 6bf2f211d..e8d57f148 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -1,11 +1,13 @@ use crate::{ config::Config, fork_service::ForkService, initialized_validators::InitializedValidators, }; +use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; use parking_lot::RwLock; use slashing_protection::{NotSafe, Safe, SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slog::{crit, error, warn, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; +use std::path::Path; use std::sync::Arc; use tempdir::TempDir; use types::{ @@ -47,7 +49,7 @@ pub struct ValidatorStore<T, E: EthSpec> { spec: Arc<ChainSpec>, log: Logger, temp_dir: Option<Arc<TempDir>>, - fork_service: ForkService<T, E>, + fork_service: ForkService<T>, _phantom: PhantomData<E>, } @@ -57,7 +59,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { config: &Config, genesis_validators_root: Hash256, spec: ChainSpec, - fork_service: ForkService<T, E>, + fork_service: ForkService<T>, log: Logger, ) -> Result<Self, String> { let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); @@ -91,6 +93,43 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { }) } + pub fn initialized_validators(&self) -> Arc<RwLock<InitializedValidators>> { + self.validators.clone() + } + + /// Insert a new validator to `self`, where the validator is represented by an EIP-2335 + /// keystore on the filesystem. + /// + /// This function includes: + /// + /// - Add the validator definition to the YAML file, saving it to the filesystem. + /// - Enable validator with the slashing protection database. + /// - If `enable == true`, start performing duties for the validator. + pub async fn add_validator_keystore<P: AsRef<Path>>( + &self, + voting_keystore_path: P, + password: ZeroizeString, + enable: bool, + ) -> Result<ValidatorDefinition, String> { + let mut validator_def = + ValidatorDefinition::new_keystore_with_password(voting_keystore_path, Some(password)) + .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; + + self.slashing_protection + .register_validator(&validator_def.voting_public_key) + .map_err(|e| format!("failed to register validator: {:?}", e))?; + + validator_def.enabled = enable; + + self.validators + .write() + .add_definition(validator_def.clone()) + .await + .map_err(|e| format!("Unable to add definition: {:?}", e))?; + + Ok(validator_def) + } + /// Register all known validators with the slashing protection database. /// /// Registration is required to protect against a lost or missing slashing database, From 255cc256234cafa62f32cb62f0a066ca2e3e077a Mon Sep 17 00:00:00 2001 From: realbigsean <seananderson33@gmail.com> Date: Thu, 1 Oct 2020 01:41:58 +0000 Subject: [PATCH 08/32] Weak subjectivity start from genesis (#1675) This commit was edited by Paul H when rebasing from master to v0.3.0-staging. Solution 2 proposed here: https://github.com/sigp/lighthouse/issues/1435#issuecomment-692317639 - Adds an optional `--wss-checkpoint` flag that takes a string `root:epoch` - Verify that the given checkpoint exists in the chain, or that the the chain syncs through this checkpoint. If not, shutdown and prompt the user to purge state before restarting. Co-authored-by: Paul Hauner <paul@paulhauner.com> --- Cargo.lock | 4 + beacon_node/Cargo.toml | 1 + beacon_node/beacon_chain/Cargo.toml | 2 + beacon_node/beacon_chain/src/beacon_chain.rs | 127 ++++++- .../beacon_chain/src/block_verification.rs | 7 + beacon_node/beacon_chain/src/builder.rs | 36 ++ beacon_node/beacon_chain/src/chain_config.rs | 6 + beacon_node/beacon_chain/src/errors.rs | 3 + beacon_node/beacon_chain/src/test_utils.rs | 50 ++- beacon_node/client/src/builder.rs | 7 + .../src/attestation_service/tests/mod.rs | 4 + .../network/src/beacon_processor/worker.rs | 1 + beacon_node/src/cli.rs | 10 + beacon_node/src/config.rs | 37 ++- consensus/fork_choice/Cargo.toml | 1 + consensus/fork_choice/tests/tests.rs | 309 +++++++++++++++++- 16 files changed, 586 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 256d91740..9abb6d62d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,6 +326,7 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive", "eth2_ssz_types", + "exit-future", "fork_choice", "futures 0.3.5", "genesis", @@ -344,6 +345,7 @@ dependencies = [ "rand 0.7.3", "rand_core 0.5.1", "rayon", + "regex", "safe_arith", "serde", "serde_derive", @@ -382,6 +384,7 @@ dependencies = [ "exit-future", "futures 0.3.5", "genesis", + "hex 0.4.2", "hyper 0.13.8", "lighthouse_version", "logging", @@ -1826,6 +1829,7 @@ dependencies = [ "beacon_chain", "eth2_ssz", "eth2_ssz_derive", + "hex 0.4.2", "proto_array", "slot_clock", "state_processing", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index deb965af1..7a5648435 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -41,3 +41,4 @@ serde = "1.0.110" clap_utils = { path = "../common/clap_utils" } hyper = "0.13.5" lighthouse_version = { path = "../common/lighthouse_version" } +hex = "0.4.2" diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 04e22f426..9694e8fb3 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -58,3 +58,5 @@ environment = { path = "../../lighthouse/environment" } bus = "2.2.3" derivative = "2.1.1" itertools = "0.9.0" +regex = "1.3.9" +exit-future = "0.2.0" diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d189b01e2..1af92df7f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -12,7 +12,6 @@ use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend}; use crate::events::{EventHandler, EventKind}; use crate::head_tracker::HeadTracker; -use crate::metrics; use crate::migrate::Migrate; use crate::naive_aggregation_pool::{Error as NaiveAggregationError, NaiveAggregationPool}; use crate::observed_attestations::{Error as AttestationObservationError, ObservedAttestations}; @@ -27,7 +26,9 @@ use crate::timeout_rw_lock::TimeoutRwLock; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::BeaconForkChoiceStore; use crate::BeaconSnapshot; +use crate::{metrics, BeaconChainError}; use fork_choice::ForkChoice; +use futures::channel::mpsc::Sender; use itertools::process_results; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::RwLock; @@ -224,6 +225,9 @@ pub struct BeaconChain<T: BeaconChainTypes> { pub(crate) validator_pubkey_cache: TimeoutRwLock<ValidatorPubkeyCache>, /// A list of any hard-coded forks that have been disabled. pub disabled_forks: Vec<String>, + /// Sender given to tasks, so that if they encounter a state in which execution cannot + /// continue they can request that everything shuts down. + pub shutdown_sender: Sender<&'static str>, /// Logging to CLI, etc. pub(crate) log: Logger, /// Arbitrary bytes included in the blocks. @@ -727,7 +731,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { /// Returns the block canonical root of the current canonical chain at a given slot. /// - /// Returns None if a block doesn't exist at the slot. + /// Returns `None` if the given slot doesn't exist in the chain. pub fn root_at_slot(&self, target_slot: Slot) -> Result<Option<Hash256>, Error> { process_results(self.rev_iter_block_roots()?, |mut iter| { iter.find(|(_, slot)| *slot == target_slot) @@ -735,6 +739,26 @@ impl<T: BeaconChainTypes> BeaconChain<T> { }) } + /// Returns the block canonical root of the current canonical chain at a given slot, starting from the given state. + /// + /// Returns `None` if the given slot doesn't exist in the chain. + pub fn root_at_slot_from_state( + &self, + target_slot: Slot, + beacon_block_root: Hash256, + state: &BeaconState<T::EthSpec>, + ) -> Result<Option<Hash256>, Error> { + let iter = BlockRootsIterator::new(self.store.clone(), state); + let iter_with_head = std::iter::once(Ok((beacon_block_root, state.slot))) + .chain(iter) + .map(|result| result.map_err(|e| e.into())); + + process_results(iter_with_head, |mut iter| { + iter.find(|(_, slot)| *slot == target_slot) + .map(|(root, _)| root) + }) + } + /// Returns the block proposer for a given slot. /// /// Information is read from the present `beacon_state` shuffling, only information from the @@ -1274,7 +1298,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { return ChainSegmentResult::Failed { imported_blocks, error: BlockError::NotFinalizedDescendant { block_parent_root }, - } + }; } // If there was an error whilst determining if the block was invalid, return that // error. @@ -1282,7 +1306,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { return ChainSegmentResult::Failed { imported_blocks, error: BlockError::BeaconChainError(e), - } + }; } // If the block was decided to be irrelevant for any other reason, don't include // this block or any of it's children in the filtered chain segment. @@ -1316,7 +1340,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { return ChainSegmentResult::Failed { imported_blocks, error, - } + }; } }; @@ -1328,7 +1352,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { return ChainSegmentResult::Failed { imported_blocks, error, - } + }; } } } @@ -1537,6 +1561,38 @@ impl<T: BeaconChainTypes> BeaconChain<T> { check_block_is_finalized_descendant::<T, _>(signed_block, &fork_choice, &self.store)?; let block = &signed_block.message; + // compare the existing finalized checkpoint with the incoming block's finalized checkpoint + let old_finalized_checkpoint = fork_choice.finalized_checkpoint(); + let new_finalized_checkpoint = state.finalized_checkpoint; + + // Only perform the weak subjectivity check if it was configured. + if let Some(wss_checkpoint) = self.config.weak_subjectivity_checkpoint { + // This ensures we only perform the check once. + if (old_finalized_checkpoint.epoch < wss_checkpoint.epoch) + && (wss_checkpoint.epoch <= new_finalized_checkpoint.epoch) + { + if let Err(e) = + self.verify_weak_subjectivity_checkpoint(wss_checkpoint, block_root, &state) + { + let mut shutdown_sender = self.shutdown_sender(); + crit!( + self.log, + "Weak subjectivity checkpoint verification failed while importing block!"; + "block_root" => format!("{:?}", block_root), + "parent_root" => format!("{:?}", block.parent_root), + "old_finalized_epoch" => format!("{:?}", old_finalized_checkpoint.epoch), + "new_finalized_epoch" => format!("{:?}", new_finalized_checkpoint.epoch), + "weak_subjectivity_epoch" => format!("{:?}", wss_checkpoint.epoch), + "error" => format!("{:?}", e), + ); + crit!(self.log, "You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network."); + shutdown_sender.try_send("Weak subjectivity checkpoint verification failed. Provided block root is not a checkpoint.") + .map_err(|err|BlockError::BeaconChainError(BeaconChainError::WeakSubjectivtyShutdownError(err)))?; + return Err(BlockError::WeakSubjectivityConflict); + } + } + } + // Register the new block with the fork choice service. { let _fork_choice_block_timer = @@ -1951,6 +2007,60 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(()) } + /// This function takes a configured weak subjectivity `Checkpoint` and the latest finalized `Checkpoint`. + /// If the weak subjectivity checkpoint and finalized checkpoint share the same epoch, we compare + /// roots. If we the weak subjectivity checkpoint is from an older epoch, we iterate back through + /// roots in the canonical chain until we reach the finalized checkpoint from the correct epoch, and + /// compare roots. This must called on startup and during verification of any block which causes a finality + /// change affecting the weak subjectivity checkpoint. + pub fn verify_weak_subjectivity_checkpoint( + &self, + wss_checkpoint: Checkpoint, + beacon_block_root: Hash256, + state: &BeaconState<T::EthSpec>, + ) -> Result<(), BeaconChainError> { + let finalized_checkpoint = state.finalized_checkpoint; + info!(self.log, "Verifying the configured weak subjectivity checkpoint"; "weak_subjectivity_epoch" => wss_checkpoint.epoch, "weak_subjectivity_root" => format!("{:?}", wss_checkpoint.root)); + // If epochs match, simply compare roots. + if wss_checkpoint.epoch == finalized_checkpoint.epoch + && wss_checkpoint.root != finalized_checkpoint.root + { + crit!( + self.log, + "Root found at the specified checkpoint differs"; + "weak_subjectivity_root" => format!("{:?}", wss_checkpoint.root), + "finalized_checkpoint_root" => format!("{:?}", finalized_checkpoint.root) + ); + return Err(BeaconChainError::WeakSubjectivtyVerificationFailure); + } else if wss_checkpoint.epoch < finalized_checkpoint.epoch { + let slot = wss_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + + // Iterate backwards through block roots from the given state. If first slot of the epoch is a skip-slot, + // this will return the root of the closest prior non-skipped slot. + match self.root_at_slot_from_state(slot, beacon_block_root, state)? { + Some(root) => { + if root != wss_checkpoint.root { + crit!( + self.log, + "Root found at the specified checkpoint differs"; + "weak_subjectivity_root" => format!("{:?}", wss_checkpoint.root), + "finalized_checkpoint_root" => format!("{:?}", finalized_checkpoint.root) + ); + return Err(BeaconChainError::WeakSubjectivtyVerificationFailure); + } + } + None => { + crit!(self.log, "The root at the start slot of the given epoch could not be found"; + "wss_checkpoint_slot" => format!("{:?}", slot)); + return Err(BeaconChainError::WeakSubjectivtyVerificationFailure); + } + } + } + Ok(()) + } + /// Called by the timer on every slot. /// /// Performs slot-based pruning. @@ -2309,6 +2419,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> { writeln!(output, "}}").unwrap(); } + /// Get a channel to request shutting down. + pub fn shutdown_sender(&self) -> Sender<&'static str> { + self.shutdown_sender.clone() + } + // Used for debugging #[allow(dead_code)] pub fn dump_dot_file(&self, file_name: &str) { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index c6d8ab02b..0e3e7db7d 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -207,6 +207,13 @@ pub enum BlockError<T: EthSpec> { /// We were unable to process this block due to an internal error. It's unclear if the block is /// valid. BeaconChainError(BeaconChainError), + /// There was an error whilst verifying weak subjectivity. This block conflicts with the + /// configured weak subjectivity checkpoint and was not imported. + /// + /// ## Peer scoring + /// + /// The block is invalid and the peer is faulty. + WeakSubjectivityConflict, } impl<T: EthSpec> std::fmt::Display for BlockError<T> { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5dbabcdd8..a251ced2d 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -18,6 +18,7 @@ use crate::{ }; use eth1::Config as Eth1Config; use fork_choice::ForkChoice; +use futures::channel::mpsc::Sender; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::RwLock; use slog::{info, Logger}; @@ -107,6 +108,7 @@ pub struct BeaconChainBuilder<T: BeaconChainTypes> { eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>, event_handler: Option<T::EventHandler>, slot_clock: Option<T::SlotClock>, + shutdown_sender: Option<Sender<&'static str>>, head_tracker: Option<HeadTracker>, data_dir: Option<PathBuf>, pubkey_cache_path: Option<PathBuf>, @@ -154,6 +156,7 @@ where eth1_chain: None, event_handler: None, slot_clock: None, + shutdown_sender: None, head_tracker: None, pubkey_cache_path: None, data_dir: None, @@ -410,6 +413,12 @@ where self } + /// Sets a `Sender` to allow the beacon chain to send shutdown signals. + pub fn shutdown_sender(mut self, sender: Sender<&'static str>) -> Self { + self.shutdown_sender = Some(sender); + self + } + /// Creates a new, empty operation pool. fn empty_op_pool(mut self) -> Self { self.op_pool = Some(OperationPool::new()); @@ -581,6 +590,9 @@ where shuffling_cache: TimeoutRwLock::new(ShufflingCache::new()), validator_pubkey_cache: TimeoutRwLock::new(validator_pubkey_cache), disabled_forks: self.disabled_forks, + shutdown_sender: self + .shutdown_sender + .ok_or_else(|| "Cannot build without a shutdown sender.".to_string())?, log: log.clone(), graffiti: self.graffiti, }; @@ -589,6 +601,27 @@ where .head() .map_err(|e| format!("Failed to get head: {:?}", e))?; + // Only perform the check if it was configured. + if let Some(wss_checkpoint) = beacon_chain.config.weak_subjectivity_checkpoint { + if let Err(e) = beacon_chain.verify_weak_subjectivity_checkpoint( + wss_checkpoint, + head.beacon_block_root, + &head.beacon_state, + ) { + crit!( + log, + "Weak subjectivity checkpoint verification failed on startup!"; + "head_block_root" => format!("{}", head.beacon_block_root), + "head_slot" => format!("{}", head.beacon_block.slot()), + "finalized_epoch" => format!("{}", head.beacon_state.finalized_checkpoint.epoch), + "wss_checkpoint_epoch" => format!("{}", wss_checkpoint.epoch), + "error" => format!("{:?}", e), + ); + crit!(log, "You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network."); + return Err(format!("Weak subjectivity verification failed: {:?}", e)); + } + } + info!( log, "Beacon chain initialized"; @@ -767,6 +800,8 @@ mod test { ) .expect("should create interop genesis state"); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let chain = BeaconChainBuilder::new(MinimalEthSpec) .logger(log.clone()) .store(Arc::new(store)) @@ -779,6 +814,7 @@ mod test { .null_event_handler() .testing_slot_clock(Duration::from_secs(1)) .expect("should configure testing slot clock") + .shutdown_sender(shutdown_tx) .build() .expect("should build"); diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 5c12bb40f..d84e62665 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,4 +1,5 @@ use serde_derive::{Deserialize, Serialize}; +use types::Checkpoint; /// There is a 693 block skip in the current canonical Medalla chain, we use 700 to be safe. pub const DEFAULT_IMPORT_BLOCK_MAX_SKIP_SLOTS: u64 = 700; @@ -10,12 +11,17 @@ pub struct ChainConfig { /// /// If `None`, there is no limit. pub import_max_skip_slots: Option<u64>, + /// A user-input `Checkpoint` that must exist in the beacon chain's sync path. + /// + /// If `None`, there is no weak subjectivity verification. + pub weak_subjectivity_checkpoint: Option<Checkpoint>, } impl Default for ChainConfig { fn default() -> Self { Self { import_max_skip_slots: Some(DEFAULT_IMPORT_BLOCK_MAX_SKIP_SLOTS), + weak_subjectivity_checkpoint: None, } } } diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 6eb7bceeb..4153ac702 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -5,6 +5,7 @@ use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_attestations::Error as ObservedAttestationsError; use crate::observed_attesters::Error as ObservedAttestersError; use crate::observed_block_producers::Error as ObservedBlockProducersError; +use futures::channel::mpsc::TrySendError; use operation_pool::OpPoolError; use safe_arith::ArithError; use ssz_types::Error as SszTypesError; @@ -87,6 +88,8 @@ pub enum BeaconChainError { shuffling_epoch: Epoch, head_block_epoch: Epoch, }, + WeakSubjectivtyVerificationFailure, + WeakSubjectivtyShutdownError(TrySendError<&'static str>), } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 2bad5f892..17dff57d1 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -8,8 +8,9 @@ use crate::{ builder::{BeaconChainBuilder, Witness}, eth1_chain::CachingEth1Backend, events::NullEventHandler, - BeaconChain, BeaconChainTypes, StateSkipConfig, + BeaconChain, BeaconChainTypes, BlockError, ChainConfig, StateSkipConfig, }; +use futures::channel::mpsc::Receiver; use genesis::interop_genesis_state; use rand::rngs::StdRng; use rand::Rng; @@ -109,6 +110,7 @@ pub struct BeaconChainHarness<T: BeaconChainTypes> { pub chain: BeaconChain<T>, pub spec: ChainSpec, pub data_dir: TempDir, + pub shutdown_receiver: Receiver<&'static str>, pub rng: StdRng, } @@ -136,6 +138,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorEphemeralHarnessType<E>> { let config = StoreConfig::default(); let store = Arc::new(HotColdDB::open_ephemeral(config, spec.clone(), log.clone()).unwrap()); + let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); let chain = BeaconChainBuilder::new(eth_spec_instance) .logger(log.clone()) @@ -153,6 +156,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorEphemeralHarnessType<E>> { .null_event_handler() .testing_slot_clock(HARNESS_SLOT_TIME) .unwrap() + .shutdown_sender(shutdown_tx) .build() .unwrap(); @@ -161,6 +165,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorEphemeralHarnessType<E>> { chain, validators_keypairs, data_dir, + shutdown_receiver, rng: make_rng(), } } @@ -186,7 +191,25 @@ impl<E: EthSpec> BeaconChainHarness<NullMigratorEphemeralHarnessType<E>> { eth_spec_instance: E, validators_keypairs: Vec<Keypair>, target_aggregators_per_committee: u64, - config: StoreConfig, + store_config: StoreConfig, + ) -> Self { + Self::new_with_chain_config( + eth_spec_instance, + validators_keypairs, + target_aggregators_per_committee, + store_config, + ChainConfig::default(), + ) + } + + /// Instantiate a new harness with `validator_count` initial validators, a custom + /// `target_aggregators_per_committee` spec value, and a `ChainConfig` + pub fn new_with_chain_config( + eth_spec_instance: E, + validators_keypairs: Vec<Keypair>, + target_aggregators_per_committee: u64, + store_config: StoreConfig, + chain_config: ChainConfig, ) -> Self { let data_dir = tempdir().expect("should create temporary data_dir"); let mut spec = E::default_spec(); @@ -197,8 +220,9 @@ impl<E: EthSpec> BeaconChainHarness<NullMigratorEphemeralHarnessType<E>> { let drain = slog_term::FullFormat::new(decorator).build(); let debug_level = slog::LevelFilter::new(drain, slog::Level::Critical); let log = slog::Logger::root(std::sync::Mutex::new(debug_level).fuse(), o!()); + let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); - let store = HotColdDB::open_ephemeral(config, spec.clone(), log.clone()).unwrap(); + let store = HotColdDB::open_ephemeral(store_config, spec.clone(), log.clone()).unwrap(); let chain = BeaconChainBuilder::new(eth_spec_instance) .logger(log) .custom_spec(spec.clone()) @@ -215,6 +239,8 @@ impl<E: EthSpec> BeaconChainHarness<NullMigratorEphemeralHarnessType<E>> { .null_event_handler() .testing_slot_clock(HARNESS_SLOT_TIME) .expect("should configure testing slot clock") + .shutdown_sender(shutdown_tx) + .chain_config(chain_config) .build() .expect("should build"); @@ -223,6 +249,7 @@ impl<E: EthSpec> BeaconChainHarness<NullMigratorEphemeralHarnessType<E>> { chain, validators_keypairs, data_dir, + shutdown_receiver, rng: make_rng(), } } @@ -242,6 +269,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { let drain = slog_term::FullFormat::new(decorator).build(); let debug_level = slog::LevelFilter::new(drain, slog::Level::Critical); let log = slog::Logger::root(std::sync::Mutex::new(debug_level).fuse(), o!()); + let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); let chain = BeaconChainBuilder::new(eth_spec_instance) .logger(log.clone()) @@ -260,6 +288,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { .null_event_handler() .testing_slot_clock(HARNESS_SLOT_TIME) .expect("should configure testing slot clock") + .shutdown_sender(shutdown_tx) .build() .expect("should build"); @@ -268,6 +297,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { chain, validators_keypairs, data_dir, + shutdown_receiver, rng: make_rng(), } } @@ -284,6 +314,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { let spec = E::default_spec(); let log = NullLoggerBuilder.build().expect("logger should build"); + let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); let chain = BeaconChainBuilder::new(eth_spec_instance) .logger(log.clone()) @@ -302,6 +333,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { .null_event_handler() .testing_slot_clock(Duration::from_secs(1)) .expect("should configure testing slot clock") + .shutdown_sender(shutdown_tx) .build() .expect("should build"); @@ -310,6 +342,7 @@ impl<E: EthSpec> BeaconChainHarness<BlockingMigratorDiskHarnessType<E>> { chain, validators_keypairs, data_dir, + shutdown_receiver, rng: make_rng(), } } @@ -697,6 +730,17 @@ where block_hash } + pub fn process_block_result( + &self, + slot: Slot, + block: SignedBeaconBlock<E>, + ) -> Result<SignedBeaconBlockHash, BlockError<E>> { + assert_eq!(self.chain.slot().unwrap(), slot); + let block_hash: SignedBeaconBlockHash = self.chain.process_block(block)?.into(); + self.chain.fork_choice().unwrap(); + Ok(block_hash) + } + pub fn process_attestations(&self, attestations: HarnessAttestations<E>) { for (unaggregated_attestations, maybe_signed_aggregate) in attestations.into_iter() { for (attestation, subnet_id) in unaggregated_attestations { diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 05cc6aa6d..97d68f407 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -438,6 +438,12 @@ where { /// Consumes the internal `BeaconChainBuilder`, attaching the resulting `BeaconChain` to self. pub fn build_beacon_chain(mut self) -> Result<Self, String> { + let context = self + .runtime_context + .as_ref() + .ok_or_else(|| "beacon_chain requires a runtime context")? + .clone(); + let chain = self .beacon_chain_builder .ok_or_else(|| "beacon_chain requires a beacon_chain_builder")? @@ -450,6 +456,7 @@ where .clone() .ok_or_else(|| "beacon_chain requires a slot clock")?, ) + .shutdown_sender(context.executor.shutdown_sender()) .build() .map_err(|e| format!("Failed to build beacon chain: {}", e))?; diff --git a/beacon_node/network/src/attestation_service/tests/mod.rs b/beacon_node/network/src/attestation_service/tests/mod.rs index 0a392727a..9ae897304 100644 --- a/beacon_node/network/src/attestation_service/tests/mod.rs +++ b/beacon_node/network/src/attestation_service/tests/mod.rs @@ -47,6 +47,9 @@ mod tests { let store = HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone(), log.clone()) .unwrap(); + + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let chain = Arc::new( BeaconChainBuilder::new(MinimalEthSpec) .logger(log.clone()) @@ -67,6 +70,7 @@ mod tests { Duration::from_secs(recent_genesis_time()), Duration::from_millis(SLOT_DURATION_MILLIS), )) + .shutdown_sender(shutdown_tx) .build() .expect("should build"), ); diff --git a/beacon_node/network/src/beacon_processor/worker.rs b/beacon_node/network/src/beacon_processor/worker.rs index 1abb2a279..44f96372f 100644 --- a/beacon_node/network/src/beacon_processor/worker.rs +++ b/beacon_node/network/src/beacon_processor/worker.rs @@ -231,6 +231,7 @@ impl<T: BeaconChainTypes> Worker<T> { | Err(e @ BlockError::BlockIsNotLaterThanParent { .. }) | Err(e @ BlockError::InvalidSignature) | Err(e @ BlockError::TooManySkippedSlots { .. }) + | Err(e @ BlockError::WeakSubjectivityConflict) | Err(e @ BlockError::GenesisBlock) => { warn!(self.log, "Could not verify block for gossip, rejecting the block"; "error" => e.to_string()); diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 2ee3fa417..5b0bd23f9 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -314,4 +314,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .default_value("700") ) + .arg( + Arg::with_name("wss-checkpoint") + .long("wss-checkpoint") + .help( + "Used to input a Weak Subjectivity State Checkpoint in `block_root:epoch_number` format,\ + where block_root is an '0x' prefixed 32-byte hex string and epoch_number is an integer." + ) + .value_name("WSS_CHECKPOINT") + .takes_value(true) + ) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index ba2dbe21a..87a4ead74 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -12,7 +12,7 @@ use std::fs; use std::net::{IpAddr, Ipv4Addr, ToSocketAddrs}; use std::net::{TcpListener, UdpSocket}; use std::path::PathBuf; -use types::{ChainSpec, EthSpec, GRAFFITI_BYTES_LEN}; +use types::{ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, GRAFFITI_BYTES_LEN}; /// Gets the fully-initialized global client. /// @@ -303,6 +303,41 @@ pub fn get_config<E: EthSpec>( client_config.graffiti.0[..trimmed_graffiti_len] .copy_from_slice(&raw_graffiti[..trimmed_graffiti_len]); + if let Some(wss_checkpoint) = cli_args.value_of("wss-checkpoint") { + let mut split = wss_checkpoint.split(':'); + let root_str = split + .next() + .ok_or_else(|| "Improperly formatted weak subjectivity checkpoint".to_string())?; + let epoch_str = split + .next() + .ok_or_else(|| "Improperly formatted weak subjectivity checkpoint".to_string())?; + + if !root_str.starts_with("0x") { + return Err( + "Unable to parse weak subjectivity checkpoint root, must have 0x prefix" + .to_string(), + ); + } + + if !root_str.chars().count() == 66 { + return Err( + "Unable to parse weak subjectivity checkpoint root, must have 32 bytes".to_string(), + ); + } + + let root = + Hash256::from_slice(&hex::decode(&root_str[2..]).map_err(|e| { + format!("Unable to parse weak subjectivity checkpoint root: {:?}", e) + })?); + let epoch = Epoch::new( + epoch_str + .parse() + .map_err(|_| "Invalid weak subjectivity checkpoint epoch".to_string())?, + ); + + client_config.chain.weak_subjectivity_checkpoint = Some(Checkpoint { epoch, root }) + } + if let Some(max_skip_slots) = cli_args.value_of("max-skip-slots") { client_config.chain.import_max_skip_slots = match max_skip_slots { "none" => None, diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index b398364c4..e07111407 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -18,3 +18,4 @@ beacon_chain = { path = "../../beacon_node/beacon_chain" } store = { path = "../../beacon_node/store" } tree_hash = { path = "../../consensus/tree_hash" } slot_clock = { path = "../../common/slot_clock" } +hex = "0.4.2" diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 86fbbd8ec..21cdfbc4a 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -4,17 +4,19 @@ use beacon_chain::{ test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, NullMigratorEphemeralHarnessType, }, - BeaconChain, BeaconChainError, BeaconForkChoiceStore, ForkChoiceError, StateSkipConfig, + BeaconChain, BeaconChainError, BeaconForkChoiceStore, ChainConfig, ForkChoiceError, + StateSkipConfig, }; use fork_choice::{ ForkChoiceStore, InvalidAttestation, InvalidBlock, QueuedAttestation, SAFE_SLOTS_TO_UPDATE_JUSTIFIED, }; +use std::fmt; use std::sync::Mutex; use store::{MemoryStore, StoreConfig}; use types::{ test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs}, - Epoch, EthSpec, IndexedAttestation, MainnetEthSpec, Slot, SubnetId, + Checkpoint, Epoch, EthSpec, IndexedAttestation, MainnetEthSpec, Slot, SubnetId, }; use types::{BeaconBlock, BeaconState, Hash256, SignedBeaconBlock}; @@ -35,6 +37,13 @@ struct ForkChoiceTest { harness: BeaconChainHarness<NullMigratorEphemeralHarnessType<E>>, } +/// Allows us to use `unwrap` in some cases. +impl fmt::Debug for ForkChoiceTest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ForkChoiceTest").finish() + } +} + impl ForkChoiceTest { /// Creates a new tester. pub fn new() -> Self { @@ -49,6 +58,20 @@ impl ForkChoiceTest { Self { harness } } + /// Creates a new tester with a custom chain config. + pub fn new_with_chain_config(chain_config: ChainConfig) -> Self { + let harness = BeaconChainHarness::new_with_chain_config( + MainnetEthSpec, + generate_deterministic_keypairs(VALIDATOR_COUNT), + // Ensure we always have an aggregator for each slot. + u64::max_value(), + StoreConfig::default(), + chain_config, + ); + + Self { harness } + } + /// Get a value from the `ForkChoice` instantiation. fn get<T, U>(&self, func: T) -> U where @@ -87,6 +110,36 @@ impl ForkChoiceTest { self } + /// Assert the given slot is greater than the head slot. + pub fn assert_finalized_epoch_is_less_than(self, epoch: Epoch) -> Self { + assert!( + self.harness + .chain + .head_info() + .unwrap() + .finalized_checkpoint + .epoch + < epoch + ); + self + } + + /// Assert there was a shutdown signal sent by the beacon chain. + pub fn assert_shutdown_signal_sent(mut self) -> Self { + self.harness.shutdown_receiver.close(); + let msg = self.harness.shutdown_receiver.try_next().unwrap(); + assert!(msg.is_some()); + self + } + + /// Assert no shutdown was signal sent by the beacon chain. + pub fn assert_shutdown_signal_not_sent(mut self) -> Self { + self.harness.shutdown_receiver.close(); + let msg = self.harness.shutdown_receiver.try_next().unwrap(); + assert!(msg.is_none()); + self + } + /// Inspect the queued attestations in fork choice. pub fn inspect_queued_attestations<F>(self, mut func: F) -> Self where @@ -116,8 +169,8 @@ impl ForkChoiceTest { self } - /// Build the chain whilst `predicate` returns `true`. - pub fn apply_blocks_while<F>(mut self, mut predicate: F) -> Self + /// Build the chain whilst `predicate` returns `true` and `process_block_result` does not error. + pub fn apply_blocks_while<F>(mut self, mut predicate: F) -> Result<Self, Self> where F: FnMut(&BeaconBlock<E>, &BeaconState<E>) -> bool, { @@ -131,13 +184,16 @@ impl ForkChoiceTest { if !predicate(&block.message, &state) { break; } - let block_hash = self.harness.process_block(slot, block.clone()); - self.harness - .attest_block(&state, block_hash, &block, &validators); - self.harness.advance_slot(); + if let Ok(block_hash) = self.harness.process_block_result(slot, block.clone()) { + self.harness + .attest_block(&state, block_hash, &block, &validators); + self.harness.advance_slot(); + } else { + return Err(self); + } } - self + Ok(self) } /// Apply `count` blocks to the chain (with attestations). @@ -409,6 +465,7 @@ fn is_safe_to_update(slot: Slot) -> bool { fn justified_checkpoint_updates_with_descendent_inside_safe_slots() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0) + .unwrap() .move_inside_safe_to_update() .assert_justified_epoch(0) .apply_blocks(1) @@ -422,6 +479,7 @@ fn justified_checkpoint_updates_with_descendent_inside_safe_slots() { fn justified_checkpoint_updates_with_descendent_outside_safe_slots() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch <= 2) + .unwrap() .move_outside_safe_to_update() .assert_justified_epoch(2) .assert_best_justified_epoch(2) @@ -436,6 +494,7 @@ fn justified_checkpoint_updates_with_descendent_outside_safe_slots() { fn justified_checkpoint_updates_first_justification_outside_safe_to_update() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0) + .unwrap() .move_to_next_unsafe_period() .assert_justified_epoch(0) .assert_best_justified_epoch(0) @@ -451,6 +510,7 @@ fn justified_checkpoint_updates_first_justification_outside_safe_to_update() { fn justified_checkpoint_updates_with_non_descendent_inside_safe_slots_without_finality() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .move_inside_safe_to_update() .assert_justified_epoch(2) @@ -476,6 +536,7 @@ fn justified_checkpoint_updates_with_non_descendent_inside_safe_slots_without_fi fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_without_finality() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .move_to_next_unsafe_period() .assert_justified_epoch(2) @@ -501,6 +562,7 @@ fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_without_f fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_with_finality() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .move_to_next_unsafe_period() .assert_justified_epoch(2) @@ -524,6 +586,7 @@ fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_with_fina fn justified_balances() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .assert_justified_epoch(2) .check_justified_balances() @@ -590,6 +653,7 @@ fn invalid_block_future_slot() { fn invalid_block_finalized_slot() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .apply_invalid_block_directly_to_fork_choice( |block, _| { @@ -619,6 +683,7 @@ fn invalid_block_finalized_descendant() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .assert_finalized_epoch(2) .apply_invalid_block_directly_to_fork_choice( @@ -904,6 +969,232 @@ fn valid_attestation_skip_across_epoch() { fn can_read_finalized_block() { ForkChoiceTest::new() .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() .apply_blocks(1) .check_finalized_block_is_accessible(); } + +#[test] +#[should_panic] +fn weak_subjectivity_fail_on_startup() { + let epoch = Epoch::new(0); + let root = Hash256::from_low_u64_le(1); + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(Checkpoint { epoch, root }), + import_max_skip_slots: None, + }; + + ForkChoiceTest::new_with_chain_config(chain_config); +} + +#[test] +fn weak_subjectivity_pass_on_startup() { + let epoch = Epoch::new(0); + let root = Hash256::zero(); + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(Checkpoint { epoch, root }), + import_max_skip_slots: None, + }; + + ForkChoiceTest::new_with_chain_config(chain_config) + .apply_blocks(E::slots_per_epoch() as usize) + .assert_shutdown_signal_not_sent(); +} + +#[test] +fn weak_subjectivity_check_passes() { + let setup_harness = ForkChoiceTest::new() + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(2); + + let checkpoint = setup_harness + .harness + .chain + .head_info() + .unwrap() + .finalized_checkpoint; + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(checkpoint), + import_max_skip_slots: None, + }; + + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(2) + .assert_shutdown_signal_not_sent(); +} + +#[test] +fn weak_subjectivity_check_fails_early_epoch() { + let setup_harness = ForkChoiceTest::new() + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(2); + + let mut checkpoint = setup_harness + .harness + .chain + .head_info() + .unwrap() + .finalized_checkpoint; + + checkpoint.epoch = checkpoint.epoch - 1; + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(checkpoint), + import_max_skip_slots: None, + }; + + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 3) + .unwrap_err() + .assert_finalized_epoch_is_less_than(checkpoint.epoch) + .assert_shutdown_signal_sent(); +} + +#[test] +fn weak_subjectivity_check_fails_late_epoch() { + let setup_harness = ForkChoiceTest::new() + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(2); + + let mut checkpoint = setup_harness + .harness + .chain + .head_info() + .unwrap() + .finalized_checkpoint; + + checkpoint.epoch = checkpoint.epoch + 1; + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(checkpoint), + import_max_skip_slots: None, + }; + + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 4) + .unwrap_err() + .assert_finalized_epoch_is_less_than(checkpoint.epoch) + .assert_shutdown_signal_sent(); +} + +#[test] +fn weak_subjectivity_check_fails_incorrect_root() { + let setup_harness = ForkChoiceTest::new() + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(2); + + let mut checkpoint = setup_harness + .harness + .chain + .head_info() + .unwrap() + .finalized_checkpoint; + + checkpoint.root = Hash256::zero(); + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(checkpoint), + import_max_skip_slots: None, + }; + + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 3) + .unwrap_err() + .assert_finalized_epoch_is_less_than(checkpoint.epoch) + .assert_shutdown_signal_sent(); +} + +#[test] +fn weak_subjectivity_check_epoch_boundary_is_skip_slot() { + let setup_harness = ForkChoiceTest::new() + // first two epochs + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap(); + + // get the head, it will become the finalized root of epoch 4 + let checkpoint_root = setup_harness.harness.chain.head_info().unwrap().block_root; + + setup_harness + // epoch 3 will be entirely skip slots + .skip_slots(E::slots_per_epoch() as usize) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 5) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(5); + + // the checkpoint at epoch 4 should become the root of last block of epoch 2 + let checkpoint = Checkpoint { + epoch: Epoch::new(4), + root: checkpoint_root, + }; + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(checkpoint), + import_max_skip_slots: None, + }; + + // recreate the chain exactly + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .skip_slots(E::slots_per_epoch() as usize) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 5) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(5) + .assert_shutdown_signal_not_sent(); +} + +#[test] +fn weak_subjectivity_check_epoch_boundary_is_skip_slot_failure() { + let setup_harness = ForkChoiceTest::new() + // first two epochs + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap(); + + // get the head, it will become the finalized root of epoch 4 + let checkpoint_root = setup_harness.harness.chain.head_info().unwrap().block_root; + + setup_harness + // epoch 3 will be entirely skip slots + .skip_slots(E::slots_per_epoch() as usize) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 5) + .unwrap() + .apply_blocks(1) + .assert_finalized_epoch(5); + + // Invalid checkpoint (epoch too early) + let checkpoint = Checkpoint { + epoch: Epoch::new(1), + root: checkpoint_root, + }; + + let chain_config = ChainConfig { + weak_subjectivity_checkpoint: Some(checkpoint), + import_max_skip_slots: None, + }; + + // recreate the chain exactly + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0) + .unwrap() + .skip_slots(E::slots_per_epoch() as usize) + .apply_blocks_while(|_, state| state.finalized_checkpoint.epoch < 6) + .unwrap_err() + .assert_finalized_epoch_is_less_than(checkpoint.epoch) + .assert_shutdown_signal_sent(); +} From 17c5da478e88427ec64b375594f12a0433dc8a96 Mon Sep 17 00:00:00 2001 From: realbigsean <seananderson33@gmail.com> Date: Fri, 2 Oct 2020 06:57:40 +0000 Subject: [PATCH 09/32] Update tiny-bip39 dependency to one implementing zeroize (#1701) ## Issue Addressed Resolves #1130 ## Proposed Changes Use the sigp fork of tiny-bip39, which includes `Zeroize` for `Mnemonic` and `Seed` ## Additional Info N/A --- Cargo.lock | 4 ++-- crypto/eth2_wallet/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9abb6d62d..27e720b9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5529,8 +5529,7 @@ dependencies = [ [[package]] name = "tiny-bip39" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0165e045cc2ae1660270ca65e1676dbaab60feb0f91b10f7d0665e9b47e31f2" +source = "git+https://github.com/sigp/tiny-bip39.git?rev=1137c32da91bd5e75db4305a84ddd15255423f7f#1137c32da91bd5e75db4305a84ddd15255423f7f" dependencies = [ "failure", "hmac 0.7.1", @@ -5540,6 +5539,7 @@ dependencies = [ "rustc-hash", "sha2 0.8.2", "unicode-normalization", + "zeroize", ] [[package]] diff --git a/crypto/eth2_wallet/Cargo.toml b/crypto/eth2_wallet/Cargo.toml index db99a3872..47e6e02d9 100644 --- a/crypto/eth2_wallet/Cargo.toml +++ b/crypto/eth2_wallet/Cargo.toml @@ -14,7 +14,7 @@ uuid = { version = "0.8", features = ["serde", "v4"] } rand = "0.7.2" eth2_keystore = { path = "../eth2_keystore" } eth2_key_derivation = { path = "../eth2_key_derivation" } -tiny-bip39 = "0.7.3" +tiny-bip39 = { git = "https://github.com/sigp/tiny-bip39.git", rev = "1137c32da91bd5e75db4305a84ddd15255423f7f" } [dev-dependencies] hex = "0.4.2" From 8fde9a401687b20f029bccdf54df4540b6e68d3c Mon Sep 17 00:00:00 2001 From: Geoffry Song <goffrie@gmail.com> Date: Fri, 2 Oct 2020 07:51:50 +0000 Subject: [PATCH 10/32] Wallet creation: Make mnemonic length configurable, default to 24 words. (#1697) ## Issue Addressed Fixes #1665. ## Proposed Changes `lighthouse account_manager wallet create` now generates a 24-word mnemonic. The user can override this by passing `--mnemonic-length 12` (or another legal bip39 length). ## Additional Info CLI `--help`: ``` --mnemonic-length <MNEMONIC_LENGTH> The number of words to use for the mnemonic phrase. [default: 24] ``` In case of an invalid argument: ``` % lighthouse account_manager wallet create --mnemonic-length 25 error: Invalid value for '--mnemonic-length <MNEMONIC_LENGTH>': Mnemonic length must be one of 12, 15, 18, 21, 24 ``` --- account_manager/src/wallet/create.rs | 30 +++++++++++++++++++++++++-- book/src/become-a-validator-source.md | 8 +++---- book/src/key-management.md | 10 +++++---- book/src/wallet-create.md | 4 ++-- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index a769cc019..1332dad44 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -23,6 +23,14 @@ pub const PASSWORD_FLAG: &str = "password-file"; pub const TYPE_FLAG: &str = "type"; pub const MNEMONIC_FLAG: &str = "mnemonic-output-path"; pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; +pub const MNEMONIC_LENGTH_FLAG: &str = "mnemonic-length"; +pub const MNEMONIC_TYPES: &[MnemonicType] = &[ + MnemonicType::Words12, + MnemonicType::Words15, + MnemonicType::Words18, + MnemonicType::Words21, + MnemonicType::Words24, +]; pub const NEW_WALLET_PASSWORD_PROMPT: &str = "Enter a password for your new wallet that is at least 12 characters long:"; pub const RETYPE_PASSWORD_PROMPT: &str = "Please re-enter your wallet's new password:"; @@ -78,6 +86,20 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long(STDIN_INPUTS_FLAG) .help("If present, read all user inputs from stdin instead of tty."), ) + .arg( + Arg::with_name(MNEMONIC_LENGTH_FLAG) + .long(MNEMONIC_LENGTH_FLAG) + .value_name("MNEMONIC_LENGTH") + .help("The number of words to use for the mnemonic phrase.") + .takes_value(true) + .validator(|len| { + match len.parse::<usize>().ok().and_then(|words| MnemonicType::for_word_count(words).ok()) { + Some(_) => Ok(()), + None => Err(format!("Mnemonic length must be one of {}", MNEMONIC_TYPES.iter().map(|t| t.word_count().to_string()).collect::<Vec<_>>().join(", "))), + } + }) + .default_value("24"), + ) } pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { @@ -86,7 +108,11 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str // Create a new random mnemonic. // // The `tiny-bip39` crate uses `thread_rng()` for this entropy. - let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); + let mnemonic_length = clap_utils::parse_required(matches, MNEMONIC_LENGTH_FLAG)?; + let mnemonic = Mnemonic::new( + MnemonicType::for_word_count(mnemonic_length).expect("Mnemonic length already validated"), + Language::English, + ); let wallet = create_wallet_from_mnemonic(matches, &wallet_base_dir.as_path(), &mnemonic)?; @@ -95,7 +121,7 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str .map_err(|e| format!("Unable to write mnemonic to {:?}: {:?}", path, e))?; } - println!("Your wallet's 12-word BIP-39 mnemonic is:"); + println!("Your wallet's {}-word BIP-39 mnemonic is:", mnemonic_length); println!(); println!("\t{}", mnemonic.phrase()); println!(); diff --git a/book/src/become-a-validator-source.md b/book/src/become-a-validator-source.md index 8b1169855..6dc661035 100644 --- a/book/src/become-a-validator-source.md +++ b/book/src/become-a-validator-source.md @@ -97,9 +97,9 @@ lighthouse --testnet medalla account wallet create You will be prompted for a wallet name and a password. The output will look like this: ``` -Your wallet's 12-word BIP-39 mnemonic is: +Your wallet's 24-word BIP-39 mnemonic is: - thank beach essence clerk gun library key grape hotel wise dutch segment + glad marble art pelican nurse large guilt response brave affair kite essence welcome gauge peace once picnic debris devote ticket blood bike solar junk This mnemonic can be used to fully restore your wallet, should you lose the JSON file or your password. @@ -114,12 +114,12 @@ a piece of paper and storing it in a safe place would be prudent. Your wallet's UUID is: - e762671a-2a33-4922-901b-62a43dbd5227 + 1c8c13d5-d065-4ef7-bad3-14e9d8146140 You do not need to backup your UUID or keep it secret. ``` -**Don't forget to make a backup** of the 12-word BIP-39 mnemonic. It can be +**Don't forget to make a backup** of the 24-word BIP-39 mnemonic. It can be used to restore your validator if there is a data loss. ### 4.2 Create a Validator from the Wallet diff --git a/book/src/key-management.md b/book/src/key-management.md index 4b03bec0e..1275057d8 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -3,7 +3,7 @@ Lighthouse uses a _hierarchical_ key management system for producing validator keys. It is hierarchical because each validator key can be _derived_ from a master key, making the validators keys _children_ of the master key. This -scheme means that a single 12-word mnemonic can be used to backup all of your +scheme means that a single 24-word mnemonic can be used to backup all of your validator keys without providing any observable link between them (i.e., it is privacy-retaining). Hierarchical key derivation schemes are common-place in cryptocurrencies, they are already used by most hardware and software wallets @@ -13,8 +13,10 @@ to secure BTC, ETH and many other coins. We defined some terms in the context of validator key management: -- **Mnemonic**: a string of 12-words that is designed to be easy to write down - and remember. E.g., _"enemy fog enlist laundry nurse hungry discover turkey holiday resemble glad discover"_. +- **Mnemonic**: a string of 24 words that is designed to be easy to write down + and remember. E.g., _"radar fly lottery mirror fat icon bachelor sadness + type exhaust mule six beef arrest you spirit clog mango snap fox citizen + already bird erase"_. - Defined in BIP-39 - **Wallet**: a wallet is a JSON file which stores an encrypted version of a mnemonic. @@ -49,7 +51,7 @@ In step (1), we created a wallet in `~/.lighthouse/{testnet}/wallets` with the n Thanks to the hierarchical key derivation scheme, we can delete all of the aforementioned directories and then regenerate them as long as we remembered -the 12-word mnemonic (we don't recommend doing this, though). +the 24-word mnemonic (we don't recommend doing this, though). Creating another validator is easy, it's just a matter of repeating step (2). The wallet keeps track of how many validators it has generated and ensures that diff --git a/book/src/wallet-create.md b/book/src/wallet-create.md index fe0ac6dc9..668b10db8 100644 --- a/book/src/wallet-create.md +++ b/book/src/wallet-create.md @@ -1,10 +1,10 @@ # Create a wallet A wallet allows for generating practically unlimited validators from an -easy-to-remember 12-word string (a mnemonic). As long as that mnemonic is +easy-to-remember 24-word string (a mnemonic). As long as that mnemonic is backed up, all validator keys can be trivially re-generated. -The 12-word string is randomly generated during wallet creation and printed out +The 24-word string is randomly generated during wallet creation and printed out to the terminal. It's important to **make one or more backups of the mnemonic** to ensure your ETH is not lost in the case of data loss. It very important to **keep your mnemonic private** as it represents the ultimate control of your From 6af3bc9ce2602411cc72e836e9fc1f4149c1d218 Mon Sep 17 00:00:00 2001 From: Sean <darcys22@gmail.com> Date: Fri, 2 Oct 2020 08:47:00 +0000 Subject: [PATCH 11/32] Add UPnP support for Lighthouse (#1587) This commit was modified by Paul H whilst rebasing master onto v0.3.0-staging Adding UPnP support will help grow the DHT by allowing NAT traversal for peers with UPnP supported routers. Using IGD library: https://docs.rs/igd/0.10.0/igd/ Adding the the libp2p tcp port and discovery udp port. If this fails it simply logs the attempt and moves on Co-authored-by: Age Manning <Age@AgeManning.com> --- Cargo.lock | 41 +++++ account_manager/Cargo.toml | 2 +- beacon_node/Cargo.toml | 2 +- beacon_node/beacon_chain/Cargo.toml | 2 +- beacon_node/client/Cargo.toml | 2 +- beacon_node/eth1/Cargo.toml | 2 +- beacon_node/eth2_libp2p/Cargo.toml | 4 +- beacon_node/eth2_libp2p/src/config.rs | 4 + beacon_node/eth2_libp2p/src/discovery/mod.rs | 56 ++++++- beacon_node/genesis/Cargo.toml | 2 +- beacon_node/http_api/Cargo.toml | 2 +- beacon_node/network/Cargo.toml | 6 +- beacon_node/network/src/lib.rs | 1 + beacon_node/network/src/nat.rs | 154 +++++++++++++++++++ beacon_node/network/src/service.rs | 53 ++++++- beacon_node/src/cli.rs | 6 + beacon_node/src/config.rs | 6 +- beacon_node/timer/Cargo.toml | 2 +- beacon_node/websocket_server/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/hashset_delay/Cargo.toml | 4 +- common/rest_types/Cargo.toml | 27 ++++ lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- lighthouse/environment/Cargo.toml | 2 +- lighthouse/environment/src/lib.rs | 4 +- testing/eth1_test_rig/Cargo.toml | 2 +- testing/simulator/Cargo.toml | 2 +- validator_client/Cargo.toml | 4 +- 29 files changed, 368 insertions(+), 32 deletions(-) create mode 100644 beacon_node/network/src/nat.rs create mode 100644 common/rest_types/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 27e720b9b..c6b5bf76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,6 +252,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0db678acb667b525ac40a324fc5f7d3390e29239b31c7327bb8157f5b4fff593" +[[package]] +name = "attohttpc" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf13118df3e3dce4b5ac930641343b91b656e4e72c8f8325838b01a4b1c9d45" +dependencies = [ + "http 0.2.1", + "log 0.4.11", + "url 2.1.1", +] + [[package]] name = "atty" version = "0.2.14" @@ -2523,6 +2534,19 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "igd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd32c880165b2f776af0b38d206d1cabaebcf46c166ac6ae004a5d45f7d48ef" +dependencies = [ + "attohttpc", + "log 0.4.11", + "rand 0.7.3", + "url 2.1.1", + "xmltree", +] + [[package]] name = "impl-codec" version = "0.4.2" @@ -3469,8 +3493,10 @@ dependencies = [ "fnv", "futures 0.3.5", "genesis", + "get_if_addrs", "hashset_delay", "hex 0.4.2", + "igd", "itertools 0.9.0", "lazy_static", "lighthouse_metrics", @@ -6746,6 +6772,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "xmltree" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76badaccb0313f1f0cb6582c2973f2dd0620f9652eb7a5ff6ced0cc3ac922b3" +dependencies = [ + "xml-rs", +] + [[package]] name = "yaml-rust" version = "0.4.4" diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 1d571489d..aaa5d52cc 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -29,7 +29,7 @@ eth2_wallet = { path = "../crypto/eth2_wallet" } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } rand = "0.7.2" validator_dir = { path = "../common/validator_dir" } -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } eth2_keystore = { path = "../crypto/eth2_keystore" } account_utils = { path = "../common/account_utils" } slashing_protection = { path = "../validator_client/slashing_protection" } diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 7a5648435..e1df8b49d 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -26,7 +26,7 @@ slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_tr slog-term = "2.5.0" slog-async = "2.5.0" ctrlc = { version = "3.1.4", features = ["termination"] } -tokio = { version = "0.2.21", features = ["time"] } +tokio = { version = "0.2.22", features = ["time"] } exit-future = "0.2.0" dirs = "2.0.2" logging = { path = "../common/logging" } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 9694e8fb3..481039c48 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -39,7 +39,7 @@ eth2_ssz_derive = "0.1.0" state_processing = { path = "../../consensus/state_processing" } tree_hash = "0.1.0" types = { path = "../../consensus/types" } -tokio = "0.2.21" +tokio = "0.2.22" eth1 = { path = "../eth1" } websocket_server = { path = "../websocket_server" } futures = "0.3.5" diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 797d7adb4..55742f851 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -27,7 +27,7 @@ error-chain = "0.12.2" serde_yaml = "0.8.11" slog = { version = "2.5.2", features = ["max_level_trace"] } slog-async = "2.5.0" -tokio = "0.2.21" +tokio = "0.2.22" dirs = "2.0.2" futures = "0.3.5" reqwest = { version = "0.10.4", features = ["native-tls-vendored"] } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 404078c80..37eea2412 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -24,7 +24,7 @@ tree_hash = "0.1.0" eth2_hashing = "0.1.0" parking_lot = "0.11.0" slog = "2.5.2" -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } state_processing = { path = "../../consensus/state_processing" } libflate = "1.0.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics"} diff --git a/beacon_node/eth2_libp2p/Cargo.toml b/beacon_node/eth2_libp2p/Cargo.toml index de916f8fa..e3f961508 100644 --- a/beacon_node/eth2_libp2p/Cargo.toml +++ b/beacon_node/eth2_libp2p/Cargo.toml @@ -15,7 +15,7 @@ eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" slog = { version = "2.5.2", features = ["max_level_trace"] } lighthouse_version = { path = "../../common/lighthouse_version" } -tokio = { version = "0.2.21", features = ["time", "macros"] } +tokio = { version = "0.2.22", features = ["time", "macros"] } futures = "0.3.5" error-chain = "0.12.2" dirs = "2.0.2" @@ -47,7 +47,7 @@ default-features = false features = ["websocket", "identify", "mplex", "noise", "gossipsub", "dns", "tcp-tokio"] [dev-dependencies] -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } slog-stdlog = "4.0.0" slog-term = "2.5.0" slog-async = "2.5.0" diff --git a/beacon_node/eth2_libp2p/src/config.rs b/beacon_node/eth2_libp2p/src/config.rs index 11bb0d362..0d974c977 100644 --- a/beacon_node/eth2_libp2p/src/config.rs +++ b/beacon_node/eth2_libp2p/src/config.rs @@ -70,6 +70,9 @@ pub struct Config { /// Disables the discovery protocol from starting. pub disable_discovery: bool, + /// Attempt to construct external port mappings with UPnP. + pub upnp_enabled: bool, + /// List of extra topics to initially subscribe to as strings. pub topics: Vec<GossipKind>, } @@ -144,6 +147,7 @@ impl Default for Config { trusted_peers: vec![], client_version: lighthouse_version::version_with_platform(), disable_discovery: false, + upnp_enabled: true, topics: Vec::new(), } } diff --git a/beacon_node/eth2_libp2p/src/discovery/mod.rs b/beacon_node/eth2_libp2p/src/discovery/mod.rs index b1a74b2d2..b065265da 100644 --- a/beacon_node/eth2_libp2p/src/discovery/mod.rs +++ b/beacon_node/eth2_libp2p/src/discovery/mod.rs @@ -155,7 +155,7 @@ pub struct Discovery<TSpec: EthSpec> { /// Indicates if the discovery service has been started. When the service is disabled, this is /// always false. - started: bool, + pub started: bool, /// Logger for the discovery behaviour. log: slog::Logger, @@ -358,6 +358,54 @@ impl<TSpec: EthSpec> Discovery<TSpec> { } } + /// Updates the local ENR TCP port. + /// There currently isn't a case to update the address here. We opt for discovery to + /// automatically update the external address. + /// + /// If the external address needs to be modified, use `update_enr_udp_socket. + pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { + self.discv5 + .enr_insert("tcp", port.to_be_bytes().into()) + .map_err(|e| format!("{:?}", e))?; + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + Ok(()) + } + + /// Updates the local ENR UDP socket. + /// + /// This is with caution. Discovery should automatically maintain this. This should only be + /// used when automatic discovery is disabled. + pub fn update_enr_udp_socket(&mut self, socket_addr: SocketAddr) -> Result<(), String> { + match socket_addr { + SocketAddr::V4(socket) => { + self.discv5 + .enr_insert("ip", socket.ip().octets().into()) + .map_err(|e| format!("{:?}", e))?; + self.discv5 + .enr_insert("udp", socket.port().to_be_bytes().into()) + .map_err(|e| format!("{:?}", e))?; + } + SocketAddr::V6(socket) => { + self.discv5 + .enr_insert("ip6", socket.ip().octets().into()) + .map_err(|e| format!("{:?}", e))?; + self.discv5 + .enr_insert("udp6", socket.port().to_be_bytes().into()) + .map_err(|e| format!("{:?}", e))?; + } + } + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + Ok(()) + } + /// Adds/Removes a subnet from the ENR Bitfield pub fn update_enr_bitfield(&mut self, subnet_id: SubnetId, value: bool) -> Result<(), String> { let id = *subnet_id as usize; @@ -390,9 +438,9 @@ impl<TSpec: EthSpec> Discovery<TSpec> { .map_err(|_| String::from("Subnet ID out of bounds, could not set subnet ID"))?; // insert the bitfield into the ENR record - let _ = self - .discv5 - .enr_insert(BITFIELD_ENR_KEY, current_bitfield.as_ssz_bytes()); + self.discv5 + .enr_insert(BITFIELD_ENR_KEY, current_bitfield.as_ssz_bytes()) + .map_err(|e| format!("{:?}", e))?; // replace the global version *self.network_globals.local_enr.write() = self.discv5.local_enr(); diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index c7e2f62ff..9510a840d 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -18,7 +18,7 @@ merkle_proof = { path = "../../consensus/merkle_proof" } eth2_ssz = "0.1.2" eth2_hashing = "0.1.0" tree_hash = "0.1.0" -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } parking_lot = "0.11.0" slog = "2.5.2" exit-future = "0.2.0" diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 828d26deb..a45ac5fec 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] warp = "0.2.5" serde = { version = "1.0.110", features = ["derive"] } -tokio = { version = "0.2.21", features = ["sync"] } +tokio = { version = "0.2.22", features = ["macros"] } parking_lot = "0.11.0" types = { path = "../../consensus/types" } hex = "0.4.2" diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index ad856a6d2..2809fd792 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -27,17 +27,17 @@ eth2_ssz_types = { path = "../../consensus/ssz_types" } tree_hash = "0.1.0" futures = "0.3.5" error-chain = "0.12.2" -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } parking_lot = "0.11.0" smallvec = "1.4.1" -# TODO: Remove rand crate for mainnet -# NOTE: why? rand = "0.7.3" fnv = "1.0.6" rlp = "0.4.5" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } environment = { path = "../../lighthouse/environment" } +igd = "0.11.1" itertools = "0.9.0" num_cpus = "1.13.0" lru_cache = { path = "../../common/lru_cache" } +get_if_addrs = "0.5.3" diff --git a/beacon_node/network/src/lib.rs b/beacon_node/network/src/lib.rs index 30795a63e..5ac74e2ed 100644 --- a/beacon_node/network/src/lib.rs +++ b/beacon_node/network/src/lib.rs @@ -8,6 +8,7 @@ pub mod service; mod attestation_service; mod beacon_processor; mod metrics; +mod nat; mod persisted_dht; mod router; mod sync; diff --git a/beacon_node/network/src/nat.rs b/beacon_node/network/src/nat.rs new file mode 100644 index 000000000..5a1edd682 --- /dev/null +++ b/beacon_node/network/src/nat.rs @@ -0,0 +1,154 @@ +//! This houses various NAT hole punching strategies. +//! +//! Currently supported strategies: +//! - UPnP + +use crate::{NetworkConfig, NetworkMessage}; +use get_if_addrs::get_if_addrs; +use slog::{debug, info, warn}; +use std::net::{IpAddr, SocketAddr, SocketAddrV4}; +use tokio::sync::mpsc; +use types::EthSpec; + +/// Configuration required to construct the UPnP port mappings. +pub struct UPnPConfig { + /// The local tcp port. + tcp_port: u16, + /// The local udp port. + udp_port: u16, + /// Whether discovery is enabled or not. + disable_discovery: bool, +} + +impl From<&NetworkConfig> for UPnPConfig { + fn from(config: &NetworkConfig) -> Self { + UPnPConfig { + tcp_port: config.libp2p_port, + udp_port: config.discovery_port, + disable_discovery: config.disable_discovery, + } + } +} + +/// Attempts to construct external port mappings with UPnP. +pub fn construct_upnp_mappings<T: EthSpec>( + config: UPnPConfig, + network_send: mpsc::UnboundedSender<NetworkMessage<T>>, + log: slog::Logger, +) { + debug!(log, "UPnP Initialising routes"); + match igd::search_gateway(Default::default()) { + Err(e) => debug!(log, "UPnP not available"; "error" => %e), + Ok(gateway) => { + // Need to find the local listening address matched with the router subnet + let mut local_ip = None; + let interfaces = match get_if_addrs() { + Ok(v) => v, + Err(e) => { + debug!(log, "UPnP failed to get local interfaces"; "error" => %e); + return; + } + }; + for interface in interfaces { + // Just use the first IP of the first interface that is not a loopback + if !interface.is_loopback() { + local_ip = Some(interface.ip()); + } + } + + if local_ip.is_none() { + debug!(log, "UPnP failed to find local IP address"); + return; + } + + let local_ip = local_ip.expect("IP exists"); + + match local_ip { + IpAddr::V4(address) => { + let libp2p_socket = SocketAddrV4::new(address, config.tcp_port); + let external_ip = gateway.get_external_ip(); + // We add specific port mappings rather than getting the router to arbitrary assign + // one. + // I've found this to be more reliable. If multiple users are behind a single + // router, they should ideally try to set different port numbers. + let tcp_socket = match gateway.add_port( + igd::PortMappingProtocol::TCP, + libp2p_socket.port(), + libp2p_socket, + 0, + "lighthouse-tcp", + ) { + Err(e) => { + debug!(log, "UPnP could not construct libp2p port route"; "error" => %e); + None + } + Ok(_) => { + info!(log, "UPnP TCP route established"; "external_socket" => format!("{}:{}", external_ip.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), config.tcp_port)); + external_ip + .as_ref() + .map(|ip| SocketAddr::new(ip.clone().into(), config.tcp_port)) + .ok() + } + }; + + let udp_socket = if !config.disable_discovery { + let discovery_socket = SocketAddrV4::new(address, config.udp_port); + match gateway.add_port( + igd::PortMappingProtocol::UDP, + discovery_socket.port(), + discovery_socket, + 0, + "lighthouse-udp", + ) { + Err(e) => { + debug!(log, "UPnP could not construct discovery port route"; "error" => %e); + None + } + Ok(_) => { + info!(log, "UPnP UDP route established"; "external_socket" => format!("{}:{}", external_ip.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), config.tcp_port)); + external_ip + .map(|ip| SocketAddr::new(ip.into(), config.tcp_port)) + .ok() + } + } + } else { + None + }; + + // report any updates to the network service. + network_send.send(NetworkMessage::UPnPMappingEstablished{ tcp_socket, udp_socket }) + .unwrap_or_else(|e| warn!(log, "Could not send message to the network service"; "error" => %e)); + } + _ => debug!(log, "UPnP no routes constructed. IPv6 not supported"), + } + } + }; +} + +/// Removes the specified TCP and UDP port mappings. +pub fn remove_mappings(tcp_port: Option<u16>, udp_port: Option<u16>, log: &slog::Logger) { + if tcp_port.is_some() || udp_port.is_some() { + debug!(log, "Removing UPnP port mappings"); + match igd::search_gateway(Default::default()) { + Ok(gateway) => { + if let Some(tcp_port) = tcp_port { + match gateway.remove_port(igd::PortMappingProtocol::TCP, tcp_port) { + Ok(()) => debug!(log, "UPnP Removed TCP port mapping"; "port" => tcp_port), + Err(e) => { + debug!(log, "UPnP Failed to remove TCP port mapping"; "port" => tcp_port, "error" => %e) + } + } + } + if let Some(udp_port) = udp_port { + match gateway.remove_port(igd::PortMappingProtocol::UDP, udp_port) { + Ok(()) => debug!(log, "UPnP Removed UDP port mapping"; "port" => udp_port), + Err(e) => { + debug!(log, "UPnP Failed to remove UDP port mapping"; "port" => udp_port, "error" => %e) + } + } + } + } + Err(e) => debug!(log, "UPnP failed to remove mappings"; "error" => %e), + } + } +} diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 1f9ddd6a0..9f5fbf355 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -16,7 +16,7 @@ use eth2_libp2p::{ use eth2_libp2p::{MessageAcceptance, Service as LibP2PService}; use futures::prelude::*; use slog::{debug, error, info, o, trace, warn}; -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use store::HotColdDB; use tokio::sync::mpsc; use tokio::time::Delay; @@ -69,6 +69,13 @@ pub enum NetworkMessage<T: EthSpec> { /// The result of the validation validation_result: MessageAcceptance, }, + /// Called if a known external TCP socket address has been updated. + UPnPMappingEstablished { + /// The external TCP address has been updated. + tcp_socket: Option<SocketAddr>, + /// The external UDP address has been updated. + udp_socket: Option<SocketAddr>, + }, /// Reports a peer to the peer manager for performing an action. ReportPeer { peer_id: PeerId, action: PeerAction }, /// Disconnect an ban a peer, providing a reason. @@ -95,6 +102,12 @@ pub struct NetworkService<T: BeaconChainTypes> { store: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>, /// A collection of global variables, accessible outside of the network service. network_globals: Arc<NetworkGlobals<T::EthSpec>>, + /// Stores potentially created UPnP mappings to be removed on shutdown. (TCP port and UDP + /// port). + upnp_mappings: (Option<u16>, Option<u16>), + /// Keeps track of if discovery is auto-updating or not. This is used to inform us if we should + /// update the UDP socket of discovery if the UPnP mappings get established. + discovery_auto_update: bool, /// A delay that expires when a new fork takes place. next_fork_update: Option<Delay>, /// A timer for updating various network metrics. @@ -116,6 +129,20 @@ impl<T: BeaconChainTypes> NetworkService<T> { let network_log = executor.log().clone(); // build the network channel let (network_send, network_recv) = mpsc::unbounded_channel::<NetworkMessage<T::EthSpec>>(); + + // try and construct UPnP port mappings if required. + let upnp_config = crate::nat::UPnPConfig::from(config); + let upnp_log = network_log.new(o!("service" => "UPnP")); + let upnp_network_send = network_send.clone(); + if config.upnp_enabled { + executor.spawn_blocking( + move || { + crate::nat::construct_upnp_mappings(upnp_config, upnp_network_send, upnp_log) + }, + "UPnP", + ); + } + // get a reference to the beacon chain store let store = beacon_chain.store.clone(); @@ -166,6 +193,8 @@ impl<T: BeaconChainTypes> NetworkService<T> { router_send, store, network_globals: network_globals.clone(), + upnp_mappings: (None, None), + discovery_auto_update: config.discv5_config.enr_update, next_fork_update, metrics_update, log: network_log, @@ -200,7 +229,6 @@ fn spawn_service<T: BeaconChainTypes>( "Persisting DHT to store"; "Number of peers" => format!("{}", enrs.len()), ); - match persist_dht::<T::EthSpec, T::HotStore, T::ColdStore>(service.store.clone(), enrs) { Err(e) => error!( service.log, @@ -213,6 +241,9 @@ fn spawn_service<T: BeaconChainTypes>( ), } + // attempt to remove port mappings + crate::nat::remove_mappings(service.upnp_mappings.0, service.upnp_mappings.1, &service.log); + info!(service.log, "Network service shutdown"); return; } @@ -239,6 +270,24 @@ fn spawn_service<T: BeaconChainTypes>( NetworkMessage::SendError{ peer_id, error, id, reason } => { service.libp2p.respond_with_error(peer_id, id, error, reason); } + NetworkMessage::UPnPMappingEstablished { tcp_socket, udp_socket} => { + service.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port())); + // If there is an external TCP port update, modify our local ENR. + if let Some(tcp_socket) = tcp_socket { + if let Err(e) = service.libp2p.swarm.peer_manager().discovery_mut().update_enr_tcp_port(tcp_socket.port()) { + warn!(service.log, "Failed to update ENR"; "error" => e); + } + } + // if the discovery service is not auto-updating, update it with the + // UPnP mappings + if !service.discovery_auto_update { + if let Some(udp_socket) = udp_socket { + if let Err(e) = service.libp2p.swarm.peer_manager().discovery_mut().update_enr_udp_socket(udp_socket) { + warn!(service.log, "Failed to update ENR"; "error" => e); + } + } + } + }, NetworkMessage::ValidationResult { propagation_source, message_id, diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 5b0bd23f9..cb7ea121d 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -75,6 +75,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("One or more comma-delimited base64-encoded ENR's to bootstrap the p2p network. Multiaddr is also supported.") .takes_value(true), ) + .arg( + Arg::with_name("disable-upnp") + .long("disable-upnp") + .help("Disables UPnP support. Setting this will prevent Lighthouse from attempting to automatically establish external port mappings.") + .takes_value(false), + ) .arg( Arg::with_name("enr-udp-port") .long("enr-udp-port") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 87a4ead74..070c99734 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -507,7 +507,7 @@ pub fn set_network_config( config.enr_address = Some(resolved_addr); } - if cli_args.is_present("disable_enr_auto_update") { + if cli_args.is_present("disable-enr-auto-update") { config.discv5_config.enr_update = false; } @@ -516,6 +516,10 @@ pub fn set_network_config( warn!(log, "Discovery is disabled. New peers will not be found"); } + if cli_args.is_present("disable-upnp") { + config.upnp_enabled = false; + } + Ok(()) } diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index f7c355302..2cee2c5de 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" beacon_chain = { path = "../beacon_chain" } types = { path = "../../consensus/types" } slot_clock = { path = "../../common/slot_clock" } -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } slog = "2.5.2" parking_lot = "0.11.0" futures = "0.3.5" diff --git a/beacon_node/websocket_server/Cargo.toml b/beacon_node/websocket_server/Cargo.toml index be914f192..00aa24973 100644 --- a/beacon_node/websocket_server/Cargo.toml +++ b/beacon_node/websocket_server/Cargo.toml @@ -12,7 +12,7 @@ serde = "1.0.110" serde_derive = "1.0.110" serde_json = "1.0.52" slog = "2.5.2" -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } types = { path = "../../consensus/types" } ws = "0.9.1" environment = { path = "../../lighthouse/environment" } diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index c788c2f11..a5895f4c3 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -13,7 +13,7 @@ eth2_testnet_config = { path = "../common/eth2_testnet_config" } eth2_ssz = { path = "../consensus/ssz" } slog = "2.5.2" sloggers = "1.0.1" -tokio = "0.2.21" +tokio = "0.2.22" log = "0.4.8" slog-term = "2.6.0" logging = { path = "../common/logging" } diff --git a/common/hashset_delay/Cargo.toml b/common/hashset_delay/Cargo.toml index 9fa9dfbd9..b577b97d6 100644 --- a/common/hashset_delay/Cargo.toml +++ b/common/hashset_delay/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] futures = "0.3.5" -tokio = { version = "0.2.21", features = ["time"] } +tokio = { version = "0.2.22", features = ["time"] } [dev-dependencies] -tokio = { version = "0.2.21", features = ["time", "rt-threaded", "macros"] } +tokio = { version = "0.2.22", features = ["time", "rt-threaded", "macros"] } diff --git a/common/rest_types/Cargo.toml b/common/rest_types/Cargo.toml new file mode 100644 index 000000000..c7e33d8b4 --- /dev/null +++ b/common/rest_types/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rest_types" +version = "0.2.0" +authors = ["Sigma Prime <contact@sigmaprime.io>"] +edition = "2018" + +[dependencies] +types = { path = "../../consensus/types" } +eth2_ssz_derive = "0.1.0" +eth2_ssz = "0.1.2" +eth2_hashing = "0.1.0" +tree_hash = "0.1.0" +state_processing = { path = "../../consensus/state_processing" } +bls = { path = "../../crypto/bls" } +serde = { version = "1.0.110", features = ["derive"] } +rayon = "1.3.0" +hyper = "0.13.5" +tokio = { version = "0.2.22", features = ["sync"] } +environment = { path = "../../lighthouse/environment" } +store = { path = "../../beacon_node/store" } +beacon_chain = { path = "../../beacon_node/beacon_chain" } +serde_json = "1.0.52" +serde_yaml = "0.8.11" + +[target.'cfg(target_os = "linux")'.dependencies] +psutil = "3.1.0" +procinfo = "0.4.2" diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 8872922bb..5df1b4d16 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -28,7 +28,7 @@ dirs = "2.0.2" genesis = { path = "../beacon_node/genesis" } deposit_contract = { path = "../common/deposit_contract" } tree_hash = "0.1.0" -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["full"] } clap_utils = { path = "../common/clap_utils" } eth2_libp2p = { path = "../beacon_node/eth2_libp2p" } validator_dir = { path = "../common/validator_dir", features = ["insecure_keys"] } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index b7468d6a4..103717506 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -14,7 +14,7 @@ milagro = ["bls/milagro"] [dependencies] beacon_node = { "path" = "../beacon_node" } -tokio = "0.2.21" +tokio = "0.2.22" slog = { version = "2.5.2", features = ["max_level_trace"] } sloggers = "1.0.0" types = { "path" = "../consensus/types" } diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 0d622990f..54a1f1f18 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" [dependencies] -tokio = { version = "0.2.21", features = ["full"] } +tokio = { version = "0.2.22", features = ["macros"] } slog = { version = "2.5.2", features = ["max_level_trace"] } sloggers = "1.0.0" types = { "path" = "../../consensus/types" } diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 81e8eee60..c175e6cab 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -30,6 +30,8 @@ mod metrics; pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; const LOG_CHANNEL_SIZE: usize = 2048; +/// The maximum time in seconds the client will wait for all internal tasks to shutdown. +const MAXIMUM_SHUTDOWN_TIME: u64 = 3; /// Builds an `Environment`. pub struct EnvironmentBuilder<E: EthSpec> { @@ -424,7 +426,7 @@ impl<E: EthSpec> Environment<E> { /// Shutdown the `tokio` runtime when all tasks are idle. pub fn shutdown_on_idle(self) { self.runtime - .shutdown_timeout(std::time::Duration::from_secs(2)) + .shutdown_timeout(std::time::Duration::from_secs(MAXIMUM_SHUTDOWN_TIME)) } /// Fire exit signal which shuts down all spawned services diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml index 12d13b451..cd0d1e858 100644 --- a/testing/eth1_test_rig/Cargo.toml +++ b/testing/eth1_test_rig/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" [dependencies] -tokio = { version = "0.2.21", features = ["time"] } +tokio = { version = "0.2.22", features = ["time"] } web3 = "0.11.0" futures = { version = "0.3.5", features = ["compat"] } types = { path = "../../consensus/types"} diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index 6670e3557..773d56bcb 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -13,7 +13,7 @@ types = { path = "../../consensus/types" } validator_client = { path = "../../validator_client" } parking_lot = "0.11.0" futures = "0.3.5" -tokio = "0.2.21" +tokio = "0.2.22" eth1_test_rig = { path = "../eth1_test_rig" } env_logger = "0.7.1" clap = "2.33.0" diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 2f1b753e1..cf1e96bef 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -9,7 +9,7 @@ name = "validator_client" path = "src/lib.rs" [dev-dependencies] -tokio = { version = "0.2.21", features = ["time", "rt-threaded", "macros"] } +tokio = { version = "0.2.22", features = ["time", "rt-threaded", "macros"] } tempfile = "3.1.0" deposit_contract = { path = "../common/deposit_contract" } @@ -29,7 +29,7 @@ serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } slog-async = "2.5.0" slog-term = "2.5.0" -tokio = { version = "0.2.21", features = ["time"] } +tokio = { version = "0.2.22", features = ["time"] } futures = { version = "0.3.5", features = ["compat"] } dirs = "2.0.2" directory = {path = "../common/directory"} From c4bd9c86e6bf23dd66bde150ef85e08caa5e4826 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Fri, 2 Oct 2020 10:46:37 +0000 Subject: [PATCH 12/32] Add check for head/target consistency (#1702) ## Issue Addressed NA ## Proposed Changes Addresses an interesting DoS vector raised by @protolambda by verifying that the head and target are consistent when processing aggregate attestations. This check prevents us from loading very old target blocks and doing lots of work to skip them to the current slot. ## Additional Info NA --- .../src/attestation_verification.rs | 97 ++++++++++++------- .../tests/attestation_verification.rs | 15 +++ 2 files changed, 79 insertions(+), 33 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 32a085902..39436bcdb 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -237,7 +237,7 @@ pub enum Error { /// The peer has sent an invalid message. InvalidTargetRoot { attestation: Hash256, - expected: Hash256, + expected: Option<Hash256>, }, /// There was an error whilst processing the attestation. It is not known if it is valid or invalid. /// @@ -349,7 +349,16 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> { // // Attestations must be for a known block. If the block is unknown, we simply drop the // attestation and do not delay consideration for later. - verify_head_block_is_known(chain, &attestation, None)?; + let head_block = verify_head_block_is_known(chain, &attestation, None)?; + + // Check the attestation target root is consistent with the head root. + // + // This check is not in the specification, however we guard against it since it opens us up + // to weird edge cases during verification. + // + // Whilst this attestation *technically* could be used to add value to a block, it is + // invalid in the spirit of the protocol. Here we choose safety over profit. + verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation)?; // Ensure that the attestation has participants. if attestation.aggregation_bits.is_zero() { @@ -475,37 +484,8 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> { let head_block = verify_head_block_is_known(chain, &attestation, chain.config.import_max_skip_slots)?; - // Check the attestation target root. - let head_block_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); - if head_block_epoch > attestation_epoch { - // The attestation points to a head block from an epoch later than the attestation. - // - // Whilst this seems clearly invalid in the "spirit of the protocol", there is nothing - // in the specification to prevent these messages from propagating. - // - // Reference: - // https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659 - } else { - let target_root = if head_block_epoch == attestation_epoch { - // If the block is in the same epoch as the attestation, then use the target root - // from the block. - head_block.target_root - } else { - // If the head block is from a previous epoch then skip slots will cause the head block - // root to become the target block root. - // - // We know the head block is from a previous epoch due to a previous check. - head_block.root - }; - - // Reject any attestation with an invalid target root. - if target_root != attestation.data.target.root { - return Err(Error::InvalidTargetRoot { - attestation: attestation.data.target.root, - expected: target_root, - }); - } - } + // Check the attestation target root is consistent with the head root. + verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation)?; let (indexed_attestation, committees_per_slot) = obtain_indexed_attestation_and_committees_per_slot(chain, &attestation)?; @@ -715,6 +695,57 @@ pub fn verify_attestation_signature<T: BeaconChainTypes>( } } +/// Verifies that the `attestation.data.target.root` is indeed the target root of the block at +/// `attestation.data.beacon_block_root`. +pub fn verify_attestation_target_root<T: EthSpec>( + head_block: &ProtoBlock, + attestation: &Attestation<T>, +) -> Result<(), Error> { + // Check the attestation target root. + let head_block_epoch = head_block.slot.epoch(T::slots_per_epoch()); + let attestation_epoch = attestation.data.slot.epoch(T::slots_per_epoch()); + if head_block_epoch > attestation_epoch { + // The epoch references an invalid head block from a future epoch. + // + // This check is not in the specification, however we guard against it since it opens us up + // to weird edge cases during verification. + // + // Whilst this attestation *technically* could be used to add value to a block, it is + // invalid in the spirit of the protocol. Here we choose safety over profit. + // + // Reference: + // https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659 + return Err(Error::InvalidTargetRoot { + attestation: attestation.data.target.root, + // It is not clear what root we should expect in this case, since the attestation is + // fundamentally invalid. + expected: None, + }); + } else { + let target_root = if head_block_epoch == attestation_epoch { + // If the block is in the same epoch as the attestation, then use the target root + // from the block. + head_block.target_root + } else { + // If the head block is from a previous epoch then skip slots will cause the head block + // root to become the target block root. + // + // We know the head block is from a previous epoch due to a previous check. + head_block.root + }; + + // Reject any attestation with an invalid target root. + if target_root != attestation.data.target.root { + return Err(Error::InvalidTargetRoot { + attestation: attestation.data.target.root, + expected: Some(target_root), + }); + } + } + + Ok(()) +} + /// Verifies all the signatures in a `SignedAggregateAndProof` using BLS batch verification. This /// includes three signatures: /// diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 35c87c0d9..4a8a071cc 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -278,6 +278,21 @@ fn aggregated_gossip_verification() { && earliest_permissible_slot == current_slot - E::slots_per_epoch() - 1 ); + /* + * This is not in the specification for aggregate attestations (only unaggregates), but we + * check it anyway to avoid weird edge cases. + */ + let unknown_root = Hash256::from_low_u64_le(424242); + assert_invalid!( + "attestation with invalid target root", + { + let mut a = valid_aggregate.clone(); + a.message.aggregate.data.target.root = unknown_root; + a + }, + AttnError::InvalidTargetRoot { .. } + ); + /* * The following test ensures: * From e3c7b586575c2667ae756cccc4af9b1ce0521ea7 Mon Sep 17 00:00:00 2001 From: divma <divma@protonmail.com> Date: Sun, 4 Oct 2020 22:50:44 +0000 Subject: [PATCH 13/32] Address a couple of TODOs (#1724) ## Issue Addressed couple of TODOs --- .../eth2_libp2p/src/behaviour/handler/mod.rs | 12 ++--------- beacon_node/eth2_libp2p/src/behaviour/mod.rs | 20 +++++-------------- .../eth2_libp2p/src/peer_manager/mod.rs | 9 --------- 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/beacon_node/eth2_libp2p/src/behaviour/handler/mod.rs b/beacon_node/eth2_libp2p/src/behaviour/handler/mod.rs index 605870d0f..538c122cc 100644 --- a/beacon_node/eth2_libp2p/src/behaviour/handler/mod.rs +++ b/beacon_node/eth2_libp2p/src/behaviour/handler/mod.rs @@ -41,15 +41,9 @@ pub enum BehaviourHandlerIn<TSpec: EthSpec> { Shutdown(Option<(RequestId, RPCRequest<TSpec>)>), } -pub enum BehaviourHandlerOut<TSpec: EthSpec> { - Delegate(Box<DelegateOut<TSpec>>), - // TODO: replace custom with events to send - Custom, -} - impl<TSpec: EthSpec> ProtocolsHandler for BehaviourHandler<TSpec> { type InEvent = BehaviourHandlerIn<TSpec>; - type OutEvent = BehaviourHandlerOut<TSpec>; + type OutEvent = DelegateOut<TSpec>; type Error = DelegateError<TSpec>; type InboundProtocol = DelegateInProto<TSpec>; type OutboundProtocol = DelegateOutProto<TSpec>; @@ -122,9 +116,7 @@ impl<TSpec: EthSpec> ProtocolsHandler for BehaviourHandler<TSpec> { match self.delegate.poll(cx) { Poll::Ready(ProtocolsHandlerEvent::Custom(event)) => { - return Poll::Ready(ProtocolsHandlerEvent::Custom( - BehaviourHandlerOut::Delegate(Box::new(event)), - )) + return Poll::Ready(ProtocolsHandlerEvent::Custom(event)) } Poll::Ready(ProtocolsHandlerEvent::Close(err)) => { return Poll::Ready(ProtocolsHandlerEvent::Close(err)) diff --git a/beacon_node/eth2_libp2p/src/behaviour/mod.rs b/beacon_node/eth2_libp2p/src/behaviour/mod.rs index 143b59f4f..72d60a553 100644 --- a/beacon_node/eth2_libp2p/src/behaviour/mod.rs +++ b/beacon_node/eth2_libp2p/src/behaviour/mod.rs @@ -5,7 +5,7 @@ use crate::types::{GossipEncoding, GossipKind, GossipTopic, SubnetDiscovery}; use crate::Eth2Enr; use crate::{error, metrics, Enr, NetworkConfig, NetworkGlobals, PubsubMessage, TopicHash}; use futures::prelude::*; -use handler::{BehaviourHandler, BehaviourHandlerIn, BehaviourHandlerOut, DelegateIn, DelegateOut}; +use handler::{BehaviourHandler, BehaviourHandlerIn, DelegateIn, DelegateOut}; use libp2p::{ core::{ connection::{ConnectedPoint, ConnectionId, ListenerId}, @@ -591,7 +591,6 @@ impl<TSpec: EthSpec> Behaviour<TSpec> { } => { if matches!(error, RPCError::HandlerRejected) { // this peer's request got canceled - // TODO: cancel processing for this request } // Inform the peer manager of the error. // An inbound error here means we sent an error to the peer, or the stream @@ -624,8 +623,6 @@ impl<TSpec: EthSpec> Behaviour<TSpec> { // TODO: inform the peer manager? } RPCRequest::Goodbye(reason) => { - // let the peer manager know this peer is in the process of disconnecting - self.peer_manager._disconnecting_peer(&peer_id); // queue for disconnection without a goodbye message debug!( self.log, "Peer sent Goodbye"; @@ -975,17 +972,11 @@ impl<TSpec: EthSpec> NetworkBehaviour for Behaviour<TSpec> { return; } + // Events comming from the handler, redirected to each behaviour match event { - // Events comming from the handler, redirected to each behaviour - BehaviourHandlerOut::Delegate(delegate) => match *delegate { - DelegateOut::Gossipsub(ev) => self.gossipsub.inject_event(peer_id, conn_id, ev), - DelegateOut::RPC(ev) => self.eth2_rpc.inject_event(peer_id, conn_id, ev), - DelegateOut::Identify(ev) => self.identify.inject_event(peer_id, conn_id, *ev), - }, - /* Custom events sent BY the handler */ - BehaviourHandlerOut::Custom => { - // TODO: implement - } + DelegateOut::Gossipsub(ev) => self.gossipsub.inject_event(peer_id, conn_id, ev), + DelegateOut::RPC(ev) => self.eth2_rpc.inject_event(peer_id, conn_id, ev), + DelegateOut::Identify(ev) => self.identify.inject_event(peer_id, conn_id, *ev), } } @@ -1003,7 +994,6 @@ impl<TSpec: EthSpec> NetworkBehaviour for Behaviour<TSpec> { self.waker = Some(cx.waker().clone()); } - // TODO: move where it's less distracting macro_rules! poll_behaviour { /* $behaviour: The sub-behaviour being polled. * $on_event_fn: Function to call if we get an event from the sub-behaviour. diff --git a/beacon_node/eth2_libp2p/src/peer_manager/mod.rs b/beacon_node/eth2_libp2p/src/peer_manager/mod.rs index d528d7e69..47dc23d7c 100644 --- a/beacon_node/eth2_libp2p/src/peer_manager/mod.rs +++ b/beacon_node/eth2_libp2p/src/peer_manager/mod.rs @@ -322,15 +322,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { self.connect_peer(peer_id, ConnectingType::OutgoingConnected { multiaddr }) } - /// Updates the database informing that a peer is being disconnected. - pub fn _disconnecting_peer(&mut self, _peer_id: &PeerId) -> bool { - // TODO: implement - // This informs the database that we are in the process of disconnecting the - // peer. Currently this state only exists for a short period of time before we force the - // disconnection. - true - } - /// Reports if a peer is banned or not. /// /// This is used to determine if we should accept incoming connections. From cee3e6483a90f034f9dc90d8cfeadbe48cf76f16 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Mon, 5 Oct 2020 00:39:30 +0000 Subject: [PATCH 14/32] Tidy some TODOs (#1721) ## Issue Addressed - Resolves #1705 ## Proposed Changes Cleans up some of my TODOs in the code base. - Adds link to issue in this repo for BLST `unsafe` block. - Confirms that the `nextaccount` field *is* required on an EIP-2386 wallet. - Reference: https://github.com/mcdee/EIPs/blob/master/EIPS/eip-2386.md#json-schema - Removes TODO about Zeroize on bip39 that was resolved in #1701 - Removes a TODO about an early randao reveal since we use the slot clock to generate the reveal: https://github.com/sigp/lighthouse/blob/c4bd9c86e6bf23dd66bde150ef85e08caa5e4826/validator_client/src/block_service.rs#L212-L220 ## Additional Info NA --- crypto/bls/src/impls/blst.rs | 2 +- crypto/eth2_wallet/src/json_wallet/mod.rs | 5 ----- crypto/eth2_wallet/src/wallet.rs | 1 - validator_client/src/validator_store.rs | 1 - 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index 3700c40f7..6a35637b4 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -66,7 +66,7 @@ pub fn verify_signature_sets<'a>( // TODO: remove this `unsafe` code-block once we get a safe option from `blst`. // - // See https://github.com/supranational/blst/issues/13 + // https://github.com/sigp/lighthouse/issues/1720 unsafe { blst::blst_scalar_from_uint64(rand_i.as_mut_ptr(), vals.as_ptr()); rands.push(rand_i.assume_init()); diff --git a/crypto/eth2_wallet/src/json_wallet/mod.rs b/crypto/eth2_wallet/src/json_wallet/mod.rs index 6c430e50d..834716fba 100644 --- a/crypto/eth2_wallet/src/json_wallet/mod.rs +++ b/crypto/eth2_wallet/src/json_wallet/mod.rs @@ -13,11 +13,6 @@ pub use uuid::Uuid; pub struct JsonWallet { pub crypto: Crypto, pub name: String, - // TODO: confirm if this field is optional or not. - // - // Reference: - // - // https://github.com/sigp/lighthouse/pull/1117#discussion_r422892396 pub nextaccount: u32, pub uuid: Uuid, pub version: Version, diff --git a/crypto/eth2_wallet/src/wallet.rs b/crypto/eth2_wallet/src/wallet.rs index e0d0d04f6..39ab816e1 100644 --- a/crypto/eth2_wallet/src/wallet.rs +++ b/crypto/eth2_wallet/src/wallet.rs @@ -66,7 +66,6 @@ impl<'a> WalletBuilder<'a> { password: &'a [u8], name: String, ) -> Result<Self, Error> { - // TODO: `bip39` does not use zeroize. Perhaps we should make a PR upstream? let seed = Bip39Seed::new(mnemonic, ""); Self::from_seed_bytes(seed.as_bytes(), password, name) diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index e8d57f148..08583f84d 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -163,7 +163,6 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { } pub fn randao_reveal(&self, validator_pubkey: &PublicKey, epoch: Epoch) -> Option<Signature> { - // TODO: check this against the slot clock to make sure it's not an early reveal? self.validators .read() .voting_keypair(validator_pubkey) From 47c921f32699e759d0f54119e6603e3f23779d13 Mon Sep 17 00:00:00 2001 From: Age Manning <Age@AgeManning.com> Date: Mon, 5 Oct 2020 05:16:27 +0000 Subject: [PATCH 15/32] Update libp2p (#1728) ## Issue Addressed N/A ## Proposed Changes Updates the libp2p dependency to the latest version ## Additional Info N/A --- Cargo.lock | 204 +++++++++++++++-------------- beacon_node/eth2_libp2p/Cargo.toml | 2 +- 2 files changed, 104 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6b5bf76f..6665ed5ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,21 +230,25 @@ dependencies = [ [[package]] name = "async-tls" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df097e3f506bec0e1a24f06bb3c962c228f36671de841ff579cb99f371772634" +checksum = "d85a97c4a0ecce878efd3f945f119c78a646d8975340bca0398f9bb05c30cc52" dependencies = [ - "futures 0.3.5", + "futures-core", + "futures-io", "rustls", "webpki", - "webpki-roots 0.19.0", + "webpki-roots", ] [[package]] name = "atomic" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f46ca51dca4837f1520754d1c8c36636356b81553d928dc9c177025369a06e" +checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281" +dependencies = [ + "autocfg 1.0.1", +] [[package]] name = "atomic-option" @@ -2805,14 +2809,14 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libp2p" -version = "0.25.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.29.0" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "atomic", "bytes 0.5.6", "futures 0.3.5", "lazy_static", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "libp2p-core-derive", "libp2p-dns", "libp2p-gossipsub", @@ -2823,46 +2827,13 @@ dependencies = [ "libp2p-tcp", "libp2p-websocket", "multihash", - "parity-multiaddr 0.9.1", - "parking_lot 0.10.2", + "parity-multiaddr 0.9.3", + "parking_lot 0.11.0", "pin-project", "smallvec 1.4.2", "wasm-timer", ] -[[package]] -name = "libp2p-core" -version = "0.21.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" -dependencies = [ - "asn1_der", - "bs58", - "ed25519-dalek", - "either", - "fnv", - "futures 0.3.5", - "futures-timer", - "lazy_static", - "libsecp256k1", - "log 0.4.11", - "multihash", - "multistream-select 0.8.2 (git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f)", - "parity-multiaddr 0.9.1", - "parking_lot 0.10.2", - "pin-project", - "prost", - "prost-build", - "rand 0.7.3", - "ring", - "rw-stream-sink", - "sha2 0.8.2", - "smallvec 1.4.2", - "thiserror", - "unsigned-varint 0.4.0", - "void", - "zeroize", -] - [[package]] name = "libp2p-core" version = "0.22.1" @@ -2880,7 +2851,7 @@ dependencies = [ "libsecp256k1", "log 0.4.11", "multihash", - "multistream-select 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "multistream-select 0.8.2", "parity-multiaddr 0.9.2", "parking_lot 0.10.2", "pin-project", @@ -2897,10 +2868,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "libp2p-core" +version = "0.22.2" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures 0.3.5", + "futures-timer", + "lazy_static", + "libsecp256k1", + "log 0.4.11", + "multihash", + "multistream-select 0.8.3", + "parity-multiaddr 0.9.3", + "parking_lot 0.11.0", + "pin-project", + "prost", + "prost-build", + "rand 0.7.3", + "ring", + "rw-stream-sink", + "sha2 0.9.1", + "smallvec 1.4.2", + "thiserror", + "unsigned-varint 0.5.1", + "void", + "zeroize", +] + [[package]] name = "libp2p-core-derive" version = "0.20.2" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "quote", "syn", @@ -2908,18 +2912,18 @@ dependencies = [ [[package]] name = "libp2p-dns" -version = "0.21.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.22.0" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "futures 0.3.5", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "log 0.4.11", ] [[package]] name = "libp2p-gossipsub" -version = "0.22.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.22.1" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "base64 0.12.3", "byteorder", @@ -2928,7 +2932,7 @@ dependencies = [ "futures 0.3.5", "futures_codec", "hex_fmt", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "libp2p-swarm", "log 0.4.11", "prost", @@ -2943,10 +2947,10 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.22.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "futures 0.3.5", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "libp2p-swarm", "log 0.4.11", "prost", @@ -2957,48 +2961,48 @@ dependencies = [ [[package]] name = "libp2p-mplex" -version = "0.21.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.23.0" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "bytes 0.5.6", "fnv", "futures 0.3.5", "futures_codec", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "log 0.4.11", - "parking_lot 0.10.2", - "unsigned-varint 0.4.0", + "parking_lot 0.11.0", + "unsigned-varint 0.5.1", ] [[package]] name = "libp2p-noise" -version = "0.23.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.24.1" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "bytes 0.5.6", - "curve25519-dalek 2.1.0", + "curve25519-dalek 3.0.0", "futures 0.3.5", "lazy_static", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "log 0.4.11", "prost", "prost-build", "rand 0.7.3", - "sha2 0.8.2", + "sha2 0.9.1", "snow", "static_assertions", - "x25519-dalek", + "x25519-dalek 1.1.0", "zeroize", ] [[package]] name = "libp2p-swarm" version = "0.22.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "either", "futures 0.3.5", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "log 0.4.11", "rand 0.7.3", "smallvec 1.4.2", @@ -3008,14 +3012,14 @@ dependencies = [ [[package]] name = "libp2p-tcp" -version = "0.21.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.22.0" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "futures 0.3.5", "futures-timer", "get_if_addrs", "ipnet", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "log 0.4.11", "socket2", "tokio 0.2.22", @@ -3023,13 +3027,13 @@ dependencies = [ [[package]] name = "libp2p-websocket" -version = "0.22.0" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.23.1" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "async-tls", "either", "futures 0.3.5", - "libp2p-core 0.21.0", + "libp2p-core 0.22.2", "log 0.4.11", "quicksink", "rustls", @@ -3037,7 +3041,7 @@ dependencies = [ "soketto", "url 2.1.1", "webpki", - "webpki-roots 0.18.0", + "webpki-roots", ] [[package]] @@ -3426,7 +3430,8 @@ dependencies = [ [[package]] name = "multistream-select" version = "0.8.2" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9157e87afbc2ef0d84cc0345423d715f445edde00141c93721c162de35a05e5" dependencies = [ "bytes 0.5.6", "futures 0.3.5", @@ -3438,16 +3443,15 @@ dependencies = [ [[package]] name = "multistream-select" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9157e87afbc2ef0d84cc0345423d715f445edde00141c93721c162de35a05e5" +version = "0.8.3" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "bytes 0.5.6", "futures 0.3.5", "log 0.4.11", "pin-project", "smallvec 1.4.2", - "unsigned-varint 0.4.0", + "unsigned-varint 0.5.1", ] [[package]] @@ -3720,8 +3724,9 @@ dependencies = [ [[package]] name = "parity-multiaddr" -version = "0.9.1" -source = "git+https://github.com/sigp/rust-libp2p?rev=03f998022ce2f566a6c6e6c4206bc0ce4d45109f#03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2165a93382a93de55868dcbfa11e4a8f99676a9164eee6a2b4a9479ad319c257" dependencies = [ "arrayref", "bs58", @@ -3737,9 +3742,8 @@ dependencies = [ [[package]] name = "parity-multiaddr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2165a93382a93de55868dcbfa11e4a8f99676a9164eee6a2b4a9479ad319c257" +version = "0.9.3" +source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "arrayref", "bs58", @@ -3749,7 +3753,7 @@ dependencies = [ "percent-encoding 2.1.0", "serde", "static_assertions", - "unsigned-varint 0.4.0", + "unsigned-varint 0.5.1", "url 2.1.1", ] @@ -5148,7 +5152,7 @@ dependencies = [ "rustc_version", "sha2 0.9.1", "subtle 2.3.0", - "x25519-dalek", + "x25519-dalek 0.6.0", ] [[package]] @@ -6210,10 +6214,6 @@ name = "unsigned-varint" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669d776983b692a906c881fcd0cfb34271a48e197e4d6cb8df32b05bfc3d3fa5" -dependencies = [ - "bytes 0.5.6", - "futures_codec", -] [[package]] name = "unsigned-varint" @@ -6619,18 +6619,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" dependencies = [ "webpki", ] @@ -6772,6 +6763,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc614d95359fd7afc321b66d2107ede58b246b844cf5d8a0adcca413e439f088" +dependencies = [ + "curve25519-dalek 3.0.0", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "xml-rs" version = "0.8.3" diff --git a/beacon_node/eth2_libp2p/Cargo.toml b/beacon_node/eth2_libp2p/Cargo.toml index e3f961508..5241208a9 100644 --- a/beacon_node/eth2_libp2p/Cargo.toml +++ b/beacon_node/eth2_libp2p/Cargo.toml @@ -42,7 +42,7 @@ regex = "1.3.9" [dependencies.libp2p] #version = "0.23.0" git = "https://github.com/sigp/rust-libp2p" -rev = "03f998022ce2f566a6c6e6c4206bc0ce4d45109f" +rev = "5a9f0819af3990cfefad528e957297af596399b4" default-features = false features = ["websocket", "identify", "mplex", "noise", "gossipsub", "dns", "tcp-tokio"] From e7eb99cb5e8f6d303028df0fe7e79417fc8cde73 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Sun, 4 Oct 2020 21:59:20 +0000 Subject: [PATCH 16/32] Use Drop impl to send worker idle message (#1718) ## Issue Addressed NA ## Proposed Changes Uses a `Drop` implementation to help ensure that `BeaconProcessor` workers are freed. This will help prevent against regression, if someone happens to add an early return and it will also help in the case of a panic. ## Additional Info NA --- .../network/src/beacon_processor/mod.rs | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/network/src/beacon_processor/mod.rs index c62bbdeb3..18983b620 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/network/src/beacon_processor/mod.rs @@ -674,7 +674,16 @@ impl<T: BeaconChainTypes> BeaconProcessor<T> { /// Spawns a blocking worker thread to process some `Work`. /// /// Sends an message on `idle_tx` when the work is complete and the task is stopping. - fn spawn_worker(&mut self, mut idle_tx: mpsc::Sender<()>, work: Work<T::EthSpec>) { + fn spawn_worker(&mut self, idle_tx: mpsc::Sender<()>, work: Work<T::EthSpec>) { + // Wrap the `idle_tx` in a struct that will fire the idle message whenever it is dropped. + // + // This helps ensure that the worker is always freed in the case of an early exit or panic. + // As such, this instantiation should happen as early in the function as possible. + let send_idle_on_drop = SendOnDrop { + tx: idle_tx, + log: self.log.clone(), + }; + let work_id = work.str_id(); let worker_timer = metrics::start_timer_vec(&metrics::BEACON_PROCESSOR_WORKER_TIME, &[work_id]); @@ -804,16 +813,40 @@ impl<T: BeaconChainTypes> BeaconProcessor<T> { "worker" => worker_id, ); - idle_tx.try_send(()).unwrap_or_else(|e| { - crit!( - log, - "Unable to free worker"; - "msg" => "failed to send idle_tx message", - "error" => e.to_string() - ) - }); + // This explicit `drop` is used to remind the programmer that this variable must + // not be dropped until the worker is complete. Dropping it early will cause the + // worker to be marked as "free" and cause an over-spawning of workers. + drop(send_idle_on_drop); }, WORKER_TASK_NAME, ); } } + +/// This struct will send a message on `self.tx` when it is dropped. An error will be logged on +/// `self.log` if the send fails (this happens when the node is shutting down). +/// +/// ## Purpose +/// +/// This is useful for ensuring that a worker-freed message is still sent if a worker panics. +/// +/// The Rust docs for `Drop` state that `Drop` is called during an unwind in a panic: +/// +/// https://doc.rust-lang.org/std/ops/trait.Drop.html#panics +pub struct SendOnDrop { + tx: mpsc::Sender<()>, + log: Logger, +} + +impl Drop for SendOnDrop { + fn drop(&mut self) { + if let Err(e) = self.tx.try_send(()) { + warn!( + self.log, + "Unable to free worker"; + "msg" => "did not free worker, shutdown may be underway", + "error" => e.to_string() + ) + } + } +} From 6997776494350d4c17be69fcf6affc848caca5d6 Mon Sep 17 00:00:00 2001 From: divma <divma@protonmail.com> Date: Sun, 4 Oct 2020 23:49:14 +0000 Subject: [PATCH 17/32] Sync fixes (#1716) ## Issue Addressed chain state inconsistencies ## Proposed Changes - a batch can be fake-failed by Range if it needs to move a peer to another chain. The peer will still send blocks/ errors / produce timeouts for those requests, so check when we get a response from the RPC that the request id matches, instead of only the peer, since a re-request can be directed to the same peer. - if an optimistic batch succeeds, store the attempt to avoid trying it again when quickly switching chains. Also, use it only if ahead of our current target, instead of the segment's start epoch --- beacon_node/network/src/sync/manager.rs | 50 +++++++-------- .../network/src/sync/network_context.rs | 4 +- .../network/src/sync/range_sync/batch.rs | 27 ++++---- .../network/src/sync/range_sync/chain.rs | 64 ++++++++++--------- .../network/src/sync/range_sync/range.rs | 6 +- 5 files changed, 81 insertions(+), 70 deletions(-) diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index a2f479292..deedc1448 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -366,7 +366,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { // check if the parent of this block isn't in our failed cache. If it is, this // chain should be dropped and the peer downscored. if self.failed_chains.contains(&block.message.parent_root) { - debug!(self.log, "Parent chain ignored due to past failure"; "block" => format!("{:?}", block.message.parent_root), "slot" => block.message.slot); + debug!(self.log, "Parent chain ignored due to past failure"; "block" => ?block.message.parent_root, "slot" => block.message.slot); if !parent_request.downloaded_blocks.is_empty() { // Add the root block to failed chains self.failed_chains @@ -392,7 +392,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { // This can be allowed as some clients may implement pruning. We mildly // tolerate this behaviour. if !single_block_request.block_returned { - warn!(self.log, "Peer didn't respond with a block it referenced"; "referenced_block_hash" => format!("{}", single_block_request.hash), "peer_id" => format!("{}", peer_id)); + warn!(self.log, "Peer didn't respond with a block it referenced"; "referenced_block_hash" => %single_block_request.hash, "peer_id" => %peer_id); self.network .report_peer(peer_id, PeerAction::MidToleranceError); } @@ -433,7 +433,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { error!( self.log, "Failed to send sync block to processor"; - "error" => format!("{:?}", e) + "error" => ?e ); return None; } @@ -465,7 +465,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { if expected_block_hash != block.canonical_root() { // The peer that sent this, sent us the wrong block. // We do not tolerate this behaviour. The peer is instantly disconnected and banned. - warn!(self.log, "Peer sent incorrect block for single block lookup"; "peer_id" => format!("{}", peer_id)); + warn!(self.log, "Peer sent incorrect block for single block lookup"; "peer_id" => %peer_id); self.network.goodbye_peer(peer_id, GoodbyeReason::Fault); return; } @@ -478,7 +478,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { // we have the correct block, try and process it match block_result { Ok(block_root) => { - info!(self.log, "Processed block"; "block" => format!("{}", block_root)); + info!(self.log, "Processed block"; "block" => %block_root); match self.chain.fork_choice() { Ok(()) => trace!( @@ -489,7 +489,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { Err(e) => error!( self.log, "Fork choice failed"; - "error" => format!("{:?}", e), + "error" => ?e, "location" => "single block" ), } @@ -502,10 +502,10 @@ impl<T: BeaconChainTypes> SyncManager<T> { trace!(self.log, "Single block lookup already known"); } Err(BlockError::BeaconChainError(e)) => { - warn!(self.log, "Unexpected block processing error"; "error" => format!("{:?}", e)); + warn!(self.log, "Unexpected block processing error"; "error" => ?e); } outcome => { - warn!(self.log, "Single block lookup failed"; "outcome" => format!("{:?}", outcome)); + warn!(self.log, "Single block lookup failed"; "outcome" => ?outcome); // This could be a range of errors. But we couldn't process the block. // For now we consider this a mid tolerance error. self.network @@ -542,7 +542,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { if self.failed_chains.contains(&block.message.parent_root) || self.failed_chains.contains(&block_root) { - debug!(self.log, "Block is from a past failed chain. Dropping"; "block_root" => format!("{:?}", block_root), "block_slot" => block.message.slot); + debug!(self.log, "Block is from a past failed chain. Dropping"; "block_root" => ?block_root, "block_slot" => block.message.slot); return; } @@ -559,7 +559,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { } } - debug!(self.log, "Unknown block received. Starting a parent lookup"; "block_slot" => block.message.slot, "block_hash" => format!("{}", block.canonical_root())); + debug!(self.log, "Unknown block received. Starting a parent lookup"; "block_slot" => block.message.slot, "block_hash" => %block.canonical_root()); let parent_request = ParentRequests { downloaded_blocks: vec![block], @@ -636,10 +636,10 @@ impl<T: BeaconChainTypes> SyncManager<T> { let head_slot = sync_info.head_slot; let finalized_epoch = sync_info.finalized_epoch; if peer_info.sync_status.update_synced(sync_info.into()) { - debug!(self.log, "Peer transitioned sync state"; "new_state" => "synced", "peer_id" => format!("{}", peer_id), "head_slot" => head_slot, "finalized_epoch" => finalized_epoch); + debug!(self.log, "Peer transitioned sync state"; "new_state" => "synced", "peer_id" => %peer_id, "head_slot" => head_slot, "finalized_epoch" => finalized_epoch); } } else { - crit!(self.log, "Status'd peer is unknown"; "peer_id" => format!("{}", peer_id)); + crit!(self.log, "Status'd peer is unknown"; "peer_id" => %peer_id); } self.update_sync_state(); } @@ -650,10 +650,10 @@ impl<T: BeaconChainTypes> SyncManager<T> { let head_slot = sync_info.head_slot; let finalized_epoch = sync_info.finalized_epoch; if peer_info.sync_status.update_advanced(sync_info.into()) { - debug!(self.log, "Peer transitioned sync state"; "new_state" => "advanced", "peer_id" => format!("{}", peer_id), "head_slot" => head_slot, "finalized_epoch" => finalized_epoch); + debug!(self.log, "Peer transitioned sync state"; "new_state" => "advanced", "peer_id" => %peer_id, "head_slot" => head_slot, "finalized_epoch" => finalized_epoch); } } else { - crit!(self.log, "Status'd peer is unknown"; "peer_id" => format!("{}", peer_id)); + crit!(self.log, "Status'd peer is unknown"; "peer_id" => %peer_id); } self.update_sync_state(); } @@ -664,10 +664,10 @@ impl<T: BeaconChainTypes> SyncManager<T> { let head_slot = sync_info.head_slot; let finalized_epoch = sync_info.finalized_epoch; if peer_info.sync_status.update_behind(sync_info.into()) { - debug!(self.log, "Peer transitioned sync state"; "new_state" => "behind", "peer_id" => format!("{}", peer_id), "head_slot" => head_slot, "finalized_epoch" => finalized_epoch); + debug!(self.log, "Peer transitioned sync state"; "new_state" => "behind", "peer_id" => %peer_id, "head_slot" => head_slot, "finalized_epoch" => finalized_epoch); } } else { - crit!(self.log, "Status'd peer is unknown"; "peer_id" => format!("{}", peer_id)); + crit!(self.log, "Status'd peer is unknown"; "peer_id" => %peer_id); } self.update_sync_state(); } @@ -675,7 +675,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { /// Updates the global sync state and logs any changes. fn update_sync_state(&mut self) { if let Some((old_state, new_state)) = self.network_globals.update_sync_state() { - info!(self.log, "Sync state updated"; "old_state" => format!("{}", old_state), "new_state" => format!("{}",new_state)); + info!(self.log, "Sync state updated"; "old_state" => %old_state, "new_state" => %new_state); // If we have become synced - Subscribe to all the core subnet topics if new_state == eth2_libp2p::types::SyncState::Synced { self.network.subscribe_core_topics(); @@ -715,9 +715,9 @@ impl<T: BeaconChainTypes> SyncManager<T> { let peer = parent_request.last_submitted_peer.clone(); warn!(self.log, "Peer sent invalid parent."; - "peer_id" => format!("{:?}",peer), - "received_block" => format!("{}", block_hash), - "expected_parent" => format!("{}", expected_hash), + "peer_id" => %peer, + "received_block" => %block_hash, + "expected_parent" => %expected_hash, ); // We try again, but downvote the peer. @@ -772,7 +772,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { error!( self.log, "Failed to send chain segment to processor"; - "error" => format!("{:?}", e) + "error" => ?e ); } } @@ -782,9 +782,9 @@ impl<T: BeaconChainTypes> SyncManager<T> { // us the last block warn!( self.log, "Invalid parent chain"; - "score_adjustment" => PeerAction::MidToleranceError.to_string(), - "outcome" => format!("{:?}", outcome), - "last_peer" => parent_request.last_submitted_peer.to_string(), + "score_adjustment" => %PeerAction::MidToleranceError, + "outcome" => ?outcome, + "last_peer" => %parent_request.last_submitted_peer, ); // Add this chain to cache of failed chains @@ -827,7 +827,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { }; debug!(self.log, "Parent import failed"; - "block" => format!("{:?}",parent_request.downloaded_blocks[0].canonical_root()), + "block" => ?parent_request.downloaded_blocks[0].canonical_root(), "ancestors_found" => parent_request.downloaded_blocks.len(), "reason" => error ); diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 715344eb1..c9b0ee058 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -87,7 +87,7 @@ impl<T: EthSpec> SyncNetworkContext<T> { request: BlocksByRangeRequest, chain_id: ChainId, batch_id: BatchId, - ) -> Result<(), &'static str> { + ) -> Result<SyncRequestId, &'static str> { trace!( self.log, "Sending BlocksByRange Request"; @@ -97,7 +97,7 @@ impl<T: EthSpec> SyncNetworkContext<T> { ); let req_id = self.send_rpc_request(peer_id, Request::BlocksByRange(request))?; self.range_requests.insert(req_id, (chain_id, batch_id)); - Ok(()) + Ok(req_id) } pub fn blocks_by_range_response( diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 532dafd2e..aa863576f 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -1,3 +1,4 @@ +use crate::sync::RequestId; use eth2_libp2p::rpc::methods::BlocksByRangeRequest; use eth2_libp2p::PeerId; use ssz::Encode; @@ -32,7 +33,7 @@ pub enum BatchState<T: EthSpec> { /// The batch has failed either downloading or processing, but can be requested again. AwaitingDownload, /// The batch is being downloaded. - Downloading(PeerId, Vec<SignedBeaconBlock<T>>), + Downloading(PeerId, Vec<SignedBeaconBlock<T>>, RequestId), /// The batch has been completely downloaded and is ready for processing. AwaitingProcessing(PeerId, Vec<SignedBeaconBlock<T>>), /// The batch is being processed. @@ -99,7 +100,7 @@ impl<T: EthSpec> BatchInfo<T> { pub fn current_peer(&self) -> Option<&PeerId> { match &self.state { BatchState::AwaitingDownload | BatchState::Failed => None, - BatchState::Downloading(peer_id, _) + BatchState::Downloading(peer_id, _, _) | BatchState::AwaitingProcessing(peer_id, _) | BatchState::Processing(Attempt { peer_id, .. }) | BatchState::AwaitingValidation(Attempt { peer_id, .. }) => Some(&peer_id), @@ -126,9 +127,9 @@ impl<T: EthSpec> BatchInfo<T> { /// Adds a block to a downloading batch. pub fn add_block(&mut self, block: SignedBeaconBlock<T>) { match self.state.poison() { - BatchState::Downloading(peer, mut blocks) => { + BatchState::Downloading(peer, mut blocks, req_id) => { blocks.push(block); - self.state = BatchState::Downloading(peer, blocks) + self.state = BatchState::Downloading(peer, blocks, req_id) } other => unreachable!("Add block for batch in wrong state: {:?}", other), } @@ -148,7 +149,7 @@ impl<T: EthSpec> BatchInfo<T> { ), > { match self.state.poison() { - BatchState::Downloading(peer, blocks) => { + BatchState::Downloading(peer, blocks, _request_id) => { // verify that blocks are in range if let Some(last_slot) = blocks.last().map(|b| b.slot()) { // the batch is non-empty @@ -189,7 +190,7 @@ impl<T: EthSpec> BatchInfo<T> { #[must_use = "Batch may have failed"] pub fn download_failed(&mut self) -> &BatchState<T> { match self.state.poison() { - BatchState::Downloading(peer, _) => { + BatchState::Downloading(peer, _, _request_id) => { // register the attempt and check if the batch can be tried again self.failed_download_attempts.push(peer); self.state = if self.failed_download_attempts.len() @@ -206,10 +207,10 @@ impl<T: EthSpec> BatchInfo<T> { } } - pub fn start_downloading_from_peer(&mut self, peer: PeerId) { + pub fn start_downloading_from_peer(&mut self, peer: PeerId, request_id: RequestId) { match self.state.poison() { BatchState::AwaitingDownload => { - self.state = BatchState::Downloading(peer, Vec::new()); + self.state = BatchState::Downloading(peer, Vec::new(), request_id); } other => unreachable!("Starting download for batch in wrong state: {:?}", other), } @@ -333,9 +334,13 @@ impl<T: EthSpec> std::fmt::Debug for BatchState<T> { BatchState::AwaitingProcessing(ref peer, ref blocks) => { write!(f, "AwaitingProcessing({}, {} blocks)", peer, blocks.len()) } - BatchState::Downloading(peer, blocks) => { - write!(f, "Downloading({}, {} blocks)", peer, blocks.len()) - } + BatchState::Downloading(peer, blocks, request_id) => write!( + f, + "Downloading({}, {} blocks, {})", + peer, + blocks.len(), + request_id + ), BatchState::Poisoned => f.write_str("Poisoned"), } } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 4decdc212..755f9e0e0 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,7 +1,7 @@ use super::batch::{BatchInfo, BatchState}; use crate::beacon_processor::ProcessId; use crate::beacon_processor::WorkEvent as BeaconWorkEvent; -use crate::sync::{network_context::SyncNetworkContext, BatchProcessResult}; +use crate::sync::{network_context::SyncNetworkContext, BatchProcessResult, RequestId}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2_libp2p::{PeerAction, PeerId}; use fnv::FnvHashMap; @@ -75,9 +75,9 @@ pub struct SyncingChain<T: BeaconChainTypes> { /// If a block is imported for this batch, the chain advances to this point. optimistic_start: Option<BatchId>, - /// When a batch for an optimistic start fails processing, it is stored to avoid trying it - /// again due to chain stopping/re-starting on chain switching. - failed_optimistic_starts: HashSet<BatchId>, + /// When a batch for an optimistic start is tried (either successful or not), it is stored to + /// avoid trying it again due to chain stopping/re-starting on chain switching. + attempted_optimistic_starts: HashSet<BatchId>, /// The current state of the chain. pub state: ChainSyncingState, @@ -135,7 +135,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { to_be_downloaded: start_epoch, processing_target: start_epoch, optimistic_start: None, - failed_optimistic_starts: HashSet::default(), + attempted_optimistic_starts: HashSet::default(), state: ChainSyncingState::Stopped, current_processing_batch: None, beacon_processor_send, @@ -200,7 +200,8 @@ impl<T: BeaconChainTypes> SyncingChain<T> { &mut self, network: &mut SyncNetworkContext<T::EthSpec>, batch_id: BatchId, - peer_id: PeerId, + peer_id: &PeerId, + request_id: RequestId, beacon_block: Option<SignedBeaconBlock<T::EthSpec>>, ) -> ProcessingResult { // check if we have this batch @@ -213,9 +214,14 @@ impl<T: BeaconChainTypes> SyncingChain<T> { Some(batch) => { // A batch could be retried without the peer failing the request (disconnecting/ // sending an error /timeout) if the peer is removed from the chain for other - // reasons. Check that this block belongs to the expected peer - if Some(&peer_id) != batch.current_peer() { - return ProcessingResult::KeepChain; + // reasons. Check that this block belongs to the expected peer, and that the + // request_id matches + if let BatchState::Downloading(expected_peer, _, expected_request_id) = + batch.state() + { + if expected_peer != peer_id || expected_request_id != &request_id { + return ProcessingResult::KeepChain; + } } batch } @@ -228,11 +234,9 @@ impl<T: BeaconChainTypes> SyncingChain<T> { } else { // A stream termination has been sent. This batch has ended. Process a completed batch. // Remove the request from the peer's active batches - let peer = batch - .current_peer() - .expect("Batch is downloading from a peer"); + self.peers - .get_mut(peer) + .get_mut(peer_id) .unwrap_or_else(|| panic!("Batch is registered for the peer")) .remove(&batch_id); @@ -442,6 +446,8 @@ impl<T: BeaconChainTypes> SyncingChain<T> { // blocks. if *was_non_empty { self.advance_chain(network, batch_id); + // we register so that on chain switching we don't try it again + self.attempted_optimistic_starts.insert(batch_id); } else if let Some(epoch) = self.optimistic_start { // check if this batch corresponds to an optimistic batch. In this case, we // reject it as an optimistic candidate since the batch was empty @@ -520,9 +526,8 @@ impl<T: BeaconChainTypes> SyncingChain<T> { redownload: bool, reason: &str, ) -> ProcessingResult { - if let Some(epoch) = self.optimistic_start { - self.optimistic_start = None; - self.failed_optimistic_starts.insert(epoch); + if let Some(epoch) = self.optimistic_start.take() { + self.attempted_optimistic_starts.insert(epoch); // if this batch is inside the current processing range, keep it, otherwise drop // it. NOTE: this is done to prevent non-sequential batches coming from optimistic // starts from filling up the buffer size @@ -628,7 +633,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { self.start_epoch = validating_epoch; self.to_be_downloaded = self.to_be_downloaded.max(validating_epoch); if self.batches.contains_key(&self.to_be_downloaded) { - // if a chain is advanced by Range beyond the previous `seld.to_be_downloaded`, we + // if a chain is advanced by Range beyond the previous `self.to_be_downloaded`, we // won't have this batch, so we need to request it. self.to_be_downloaded += EPOCHS_PER_BATCH; } @@ -732,8 +737,8 @@ impl<T: BeaconChainTypes> SyncingChain<T> { // advance the chain to the new validating epoch self.advance_chain(network, validating_epoch); if self.optimistic_start.is_none() - && optimistic_epoch > self.start_epoch - && !self.failed_optimistic_starts.contains(&optimistic_epoch) + && optimistic_epoch > self.processing_target + && !self.attempted_optimistic_starts.contains(&optimistic_epoch) { self.optimistic_start = Some(optimistic_epoch); } @@ -782,21 +787,21 @@ impl<T: BeaconChainTypes> SyncingChain<T> { &mut self, network: &mut SyncNetworkContext<T::EthSpec>, batch_id: BatchId, - peer_id: PeerId, + peer_id: &PeerId, + request_id: RequestId, ) -> ProcessingResult { if let Some(batch) = self.batches.get_mut(&batch_id) { // A batch could be retried without the peer failing the request (disconnecting/ // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer - if Some(&peer_id) != batch.current_peer() { - return ProcessingResult::KeepChain; + if let BatchState::Downloading(expected_peer, _, expected_request_id) = batch.state() { + if expected_peer != peer_id || expected_request_id != &request_id { + return ProcessingResult::KeepChain; + } } debug!(self.log, "Batch failed. RPC Error"; "batch_epoch" => batch_id); - let failed_peer = batch - .current_peer() - .expect("Batch is downloading from a peer"); self.peers - .get_mut(failed_peer) + .get_mut(peer_id) .expect("Peer belongs to the chain") .remove(&batch_id); if let BatchState::Failed = batch.download_failed() { @@ -851,10 +856,10 @@ impl<T: BeaconChainTypes> SyncingChain<T> { ) -> ProcessingResult { if let Some(batch) = self.batches.get_mut(&batch_id) { let request = batch.to_blocks_by_range_request(); - // inform the batch about the new request - batch.start_downloading_from_peer(peer.clone()); match network.blocks_by_range_request(peer.clone(), request, self.id, batch_id) { - Ok(()) => { + Ok(request_id) => { + // inform the batch about the new request + batch.start_downloading_from_peer(peer.clone(), request_id); if self .optimistic_start .map(|epoch| epoch == batch_id) @@ -876,6 +881,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { warn!(self.log, "Could not send batch request"; "batch_id" => batch_id, "error" => e, &batch); // register the failed download and check if the batch can be retried + batch.start_downloading_from_peer(peer.clone(), 1); // fake request_id is not relevant self.peers .get_mut(&peer) .expect("peer belongs to the peer pool") diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 6847838e0..48a9bd5d4 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -214,7 +214,7 @@ impl<T: BeaconChainTypes> RangeSync<T> { { // check if this chunk removes the chain match self.chains.call_by_id(chain_id, |chain| { - chain.on_block_response(network, batch_id, peer_id, beacon_block) + chain.on_block_response(network, batch_id, &peer_id, request_id, beacon_block) }) { Ok((removed_chain, sync_type)) => { if let Some(removed_chain) = removed_chain { @@ -228,7 +228,7 @@ impl<T: BeaconChainTypes> RangeSync<T> { } } } else { - warn!(self.log, "Response/Error for non registered request"; "request_id" => request_id) + debug!(self.log, "Response/Error for non registered request"; "request_id" => request_id) } } @@ -337,7 +337,7 @@ impl<T: BeaconChainTypes> RangeSync<T> { if let Some((chain_id, batch_id)) = network.blocks_by_range_response(request_id, true) { // check that this request is pending match self.chains.call_by_id(chain_id, |chain| { - chain.inject_error(network, batch_id, peer_id) + chain.inject_error(network, batch_id, &peer_id, request_id) }) { Ok((removed_chain, sync_type)) => { if let Some(removed_chain) = removed_chain { From a8c5af8874fb2ac4347e39d75b1fc0e392fad303 Mon Sep 17 00:00:00 2001 From: Age Manning <Age@AgeManning.com> Date: Sun, 4 Oct 2020 23:49:16 +0000 Subject: [PATCH 18/32] Increase content-id length (#1725) ## Issue Addressed N/A ## Proposed Changes Increase gossipsub's content-id length to the full 32 byte hash. ## Additional Info N/A --- beacon_node/eth2_libp2p/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/eth2_libp2p/src/config.rs b/beacon_node/eth2_libp2p/src/config.rs index 0d974c977..f3fbbda53 100644 --- a/beacon_node/eth2_libp2p/src/config.rs +++ b/beacon_node/eth2_libp2p/src/config.rs @@ -92,7 +92,7 @@ impl Default for Config { // The function used to generate a gossipsub message id // We use the first 8 bytes of SHA256(data) for content addressing let gossip_message_id = - |message: &GossipsubMessage| MessageId::from(&Sha256::digest(&message.data)[..8]); + |message: &GossipsubMessage| MessageId::from(&Sha256::digest(&message.data)[..]); // gossipsub configuration // Note: The topics by default are sent as plain strings. Hashes are an optional From cf74e0baed0895d8762a729d41f6587c37f478e6 Mon Sep 17 00:00:00 2001 From: Justin <drakefjustin@gmail.com> Date: Mon, 5 Oct 2020 03:20:53 +0000 Subject: [PATCH 19/32] Document need for port 9000 to be open (fix #730) (#731) Co-authored-by: Age Manning <Age@AgeManning.com> Edited by Paul H when cherry-picking from master to v0.3.0-staging --- book/src/SUMMARY.md | 1 + book/src/advanced_networking.md | 77 +++++++++++++++++++++++++++ book/src/become-a-validator-docker.md | 5 ++ book/src/become-a-validator-source.md | 6 +++ book/src/become-a-validator.md | 1 - book/src/faq.md | 45 ++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 book/src/advanced_networking.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index a13ced95a..1ecfa9213 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -26,6 +26,7 @@ * [Advanced Usage](./advanced.md) * [Database Configuration](./advanced_database.md) * [Local Testnets](./local-testnets.md) + * [Advanced Networking](./advanced_networking.md) * [Contributing](./contributing.md) * [Development Environment](./setup.md) * [FAQs](./faq.md) diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md new file mode 100644 index 000000000..e70decbfb --- /dev/null +++ b/book/src/advanced_networking.md @@ -0,0 +1,77 @@ +# Advanced Networking + +Lighthouse's networking stack has a number of configurable parameters that can +be adjusted to handle a variety of network situations. This section outlines +some of these configuration parameters and their consequences at the networking +level and their general intended use. + + +### Target Peers + +The beacon node has a `--target-peers` CLI parameter. This allows you to +instruct the beacon node how many peers it should try to find and maintain. +Lighthouse allows an additional 10% of this value for nodes to connect to us. +Every 30 seconds, the excess peers are pruned. Lighthouse removes the +worst-performing peers and maintains the best performing peers. + +It may be counter-intuitive, but having a very large peer count will likely +have a degraded performance for a beacon node in normal operation and during +sync. + +Having a large peer count means that your node must act as an honest RPC server +to all your connected peers. If there are many that are syncing, they will +often be requesting a large number of blocks from your node. This means you +node must perform a lot of work reading and responding to these peers. If you +node is over-loaded with peers and cannot respond in time, other Lighthouse +peers will consider you non-performant and disfavour you from their peer +stores. You node will also have to handle and manage the gossip and extra +bandwidth that comes from having these extra peers. Having a non-responsive +node (due to overloading of connected peers), degrades the network as a whole. + +It is often the belief that a higher peer counts will improve sync times. +Beyond a handful of peers, this is not true. On all current tested networks, +the bottleneck for syncing is not the network bandwidth of downloading blocks, +rather it is the CPU load of processing the blocks themselves. Most of the +time, the network is idle, waiting for blocks to be processed. Having a very +large peer count will not speed up sync. + +For these reasons, we recommend users do not modify the `--target-peer` count +drastically and use the (recommended) default. + + +### NAT Traversal (Port Forwarding) + +Lighthouse, by default, used port 9000 for both TCP and UDP. Lighthouse will +still function if it is behind a NAT without any port mappings. Although +Lighthouse still functions, we recommend that some mechanism is used to ensure +that your Lighthouse node is publicly accessible. This will typically improve +your peer count, allow the scoring system to find the best/most favourable +peers for your node and overall improve the eth2 network. + +Lighthouse currently supports UPnP. If UPnP is enabled on your router, +Lighthouse will automatically establish the port mappings for you (the beacon +node will inform you of established routes in this case). If UPnP is not +enabled, we recommend you manually set up port mappings to both of Lighthouse's +TCP and UDP ports (9000 by default). + +### ENR Configuration + +Lighthouse has a number of CLI parameters for constructing and modifying the +local Ethereum Node Record (ENR). Examples are `--enr-address`, +`--enr-udp-port`, `--enr-tcp-port` and `--disable-enr-auto-update`. These +settings allow you construct your initial ENR. Their primary intention is for +setting up boot-like nodes and having a contactable ENR on boot. On normal +operation of a Lighthouse node, none of these flags need to be set. Setting +these flags incorrectly can lead to your node being incorrectly added to the +global DHT which will degrades the discovery process for all Eth2 peers. + +The ENR of a Lighthouse node is initially set to be non-contactable. The +in-built discovery mechanism can determine if you node is publicly accessible, +and if it is, it will update your ENR to the correct public IP and port address +(meaning you do not need to set it manually). Lighthouse persists its ENR, so +on reboot it will re-load the settings it had discovered previously. + +Modifying the ENR settings can degrade the discovery of your node making it +harder for peers to find you or potentially making it harder for other peers to +find each other. We recommend not touching these settings unless for a more +advanced use case. diff --git a/book/src/become-a-validator-docker.md b/book/src/become-a-validator-docker.md index fd3331b8a..ce45996fd 100644 --- a/book/src/become-a-validator-docker.md +++ b/book/src/become-a-validator-docker.md @@ -83,6 +83,11 @@ This is one of the earlier logs outputted, so you may have to scroll up or perfo > the genesis of the network is known (approx 2 days before the network > launches). +> Note: Docker exposes ports TCP 9000 and UDP 9000 by default. Although not +> strictly required, we recommend setting up port forwards to expose these +> ports publicly. For more information see the FAQ or the [Advanced Networking](advanced_networking.html) +> section + To find an estimate for how long your beacon node will take to finish syncing, look for logs that look like this: ```bash diff --git a/book/src/become-a-validator-source.md b/book/src/become-a-validator-source.md index 6dc661035..b1a8db045 100644 --- a/book/src/become-a-validator-source.md +++ b/book/src/become-a-validator-source.md @@ -55,6 +55,12 @@ Start your beacon node with: > Current values are either `altona` or `medalla`. This is true for all the > following commands in this document. +> Note: Lighthouse, by default, opens port 9000 over TCP and UDP. Although not +> strictly required, we recommend setting up port forwards to expose these +> ports publicly. For more information see the FAQ or the [Advanced Networking](advanced_networking.html) +> section + + You can also pass an external http endpoint (e.g. Infura) for the Eth1 node using the `--eth1-endpoint` flag: ```bash diff --git a/book/src/become-a-validator.md b/book/src/become-a-validator.md index bab262343..a8f01c38b 100644 --- a/book/src/become-a-validator.md +++ b/book/src/become-a-validator.md @@ -26,7 +26,6 @@ Once you've completed **either one** of these steps, you can move onto the next > Take note when running Lighthouse. Use the --testnet parameter to specify the testnet you whish to participate in. Medalla is currently the default, so make sure to use --testnet altona to join the Altona testnet. - ## 2. Submit your deposit to Goerli <div class="form-signin" id="uploadDiv"> diff --git a/book/src/faq.md b/book/src/faq.md index 3839d1ecd..4038ec7a8 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -79,3 +79,48 @@ repeats until the queue is cleared. Once a validator has been activated, there's no more waiting! It's time to produce blocks and attestations! + +### 3. Do I need to set up any port mappings + +It is not strictly required to open any ports for Lighthouse to connect and +participate in the network. Lighthouse should work out-of-the-box. However, if +your node is not publicly accessible (you are behind a NAT or router that has +not been configured to allow access to Lighthouse ports) you will only be able +to reach peers who have a set up that is publicly accessible. + +There are a number of undesired consequences of not making your Lighthouse node +publicly accessible. + +Firstly, it will make it more difficult for your node to find peers, as your +node will not be added to the global DHT and other peers will not be able +to initiate connections with you. +Secondly, the peers in your peer store are more likely to end connections with +you and be less performant as these peers will likely be overloaded with +subscribing peers. The reason being, that peers that have correct port +forwarding (publicly accessible) are in higher demand than regular peers as other nodes behind NAT's +will also be looking for these peers. +Finally, not making your node publicly accessible degrades the overall network, making it more difficult for other +peers to join and degrades the overall connectivity of the global network. + +For these reasons, we recommend that you make your node publicly accessible. + +Lighthouse supports UPnP. If you are behind a NAT with a router that supports +UPnP you can simply ensure UPnP is enabled (Lighthouse will inform you in its +initial logs if a route has been established). You can also manually set up +port mappings in your router to your local Lighthouse instance. By default, +Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will +make your Lighthouse node maximally contactable. + +#### 4. I have a low peer count and it is not increasing + +If you cannot find *ANY* peers at all. It is likely that you have incorrect +testnet configuration settings. Ensure that the network you wish to connect to +is correct (the beacon node outputs the network it is connecting to in the +initial boot-up log lines). On top of this, ensure that you are not using the +same `datadir` as a previous network. I.e if you have been running the +`medalla` testnet and are now trying to join a new testnet but using the same +`datadir` (the `datadir` is also printed out in the beacon node's logs on +boot-up). + +If you find yourself with a low peer count and is not reaching the target you +expect. Try setting up the correct port forwards as described in `3.` above. From 113758a4f573ca8d9ae6d28b4c58fcf1592d5de7 Mon Sep 17 00:00:00 2001 From: divma <divma@protonmail.com> Date: Mon, 5 Oct 2020 04:02:09 +0000 Subject: [PATCH 20/32] From panic to crit (#1726) ## Issue Addressed Downgrade inconsistent chain segment states from `panic` to `crit`. I don't love this solution but since range can always bounce back from any of those, we don't panic. Co-authored-by: Age Manning <Age@AgeManning.com> --- .../network/src/sync/range_sync/batch.rs | 81 +++++++++++++------ .../network/src/sync/range_sync/chain.rs | 54 ++++++------- 2 files changed, 85 insertions(+), 50 deletions(-) diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index aa863576f..b51b20897 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -1,6 +1,7 @@ use crate::sync::RequestId; use eth2_libp2p::rpc::methods::BlocksByRangeRequest; use eth2_libp2p::PeerId; +use slog::{crit, warn, Logger}; use ssz::Encode; use std::collections::HashSet; use std::hash::{Hash, Hasher}; @@ -125,13 +126,17 @@ impl<T: EthSpec> BatchInfo<T> { } /// Adds a block to a downloading batch. - pub fn add_block(&mut self, block: SignedBeaconBlock<T>) { + pub fn add_block(&mut self, block: SignedBeaconBlock<T>, logger: &Logger) { match self.state.poison() { BatchState::Downloading(peer, mut blocks, req_id) => { blocks.push(block); self.state = BatchState::Downloading(peer, blocks, req_id) } - other => unreachable!("Add block for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Add block for batch in wrong state"; "state" => ?other); + self.state = other + } } } @@ -140,14 +145,8 @@ impl<T: EthSpec> BatchInfo<T> { #[must_use = "Batch may have failed"] pub fn download_completed( &mut self, - ) -> Result< - usize, /* Received blocks */ - ( - Slot, /* expected slot */ - Slot, /* received slot */ - &BatchState<T>, - ), - > { + logger: &Logger, + ) -> Result<usize /* Received blocks */, &BatchState<T>> { match self.state.poison() { BatchState::Downloading(peer, blocks, _request_id) => { // verify that blocks are in range @@ -163,7 +162,7 @@ impl<T: EthSpec> BatchInfo<T> { None }; - if let Some(range) = failed_range { + if let Some((expected, received)) = failed_range { // this is a failed download, register the attempt and check if the batch // can be tried again self.failed_download_attempts.push(peer); @@ -175,7 +174,9 @@ impl<T: EthSpec> BatchInfo<T> { // drop the blocks BatchState::AwaitingDownload }; - return Err((range.0, range.1, &self.state)); + warn!(logger, "Batch received out of range blocks"; + &self, "expected" => expected, "received" => received); + return Err(&self.state); } } @@ -183,12 +184,17 @@ impl<T: EthSpec> BatchInfo<T> { self.state = BatchState::AwaitingProcessing(peer, blocks); Ok(received) } - other => unreachable!("Download completed for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Download completed for batch in wrong state"; "state" => ?other); + self.state = other; + Err(&self.state) + } } } #[must_use = "Batch may have failed"] - pub fn download_failed(&mut self) -> &BatchState<T> { + pub fn download_failed(&mut self, logger: &Logger) -> &BatchState<T> { match self.state.poison() { BatchState::Downloading(peer, _, _request_id) => { // register the attempt and check if the batch can be tried again @@ -203,31 +209,50 @@ impl<T: EthSpec> BatchInfo<T> { }; &self.state } - other => unreachable!("Download failed for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Download failed for batch in wrong state"; "state" => ?other); + self.state = other; + &self.state + } } } - pub fn start_downloading_from_peer(&mut self, peer: PeerId, request_id: RequestId) { + pub fn start_downloading_from_peer( + &mut self, + peer: PeerId, + request_id: RequestId, + logger: &Logger, + ) { match self.state.poison() { BatchState::AwaitingDownload => { self.state = BatchState::Downloading(peer, Vec::new(), request_id); } - other => unreachable!("Starting download for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Starting download for batch in wrong state"; "state" => ?other); + self.state = other + } } } - pub fn start_processing(&mut self) -> Vec<SignedBeaconBlock<T>> { + pub fn start_processing(&mut self, logger: &Logger) -> Vec<SignedBeaconBlock<T>> { match self.state.poison() { BatchState::AwaitingProcessing(peer, blocks) => { self.state = BatchState::Processing(Attempt::new(peer, &blocks)); blocks } - other => unreachable!("Start processing for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Starting procesing batch in wrong state"; "state" => ?other); + self.state = other; + vec![] + } } } #[must_use = "Batch may have failed"] - pub fn processing_completed(&mut self, was_sucessful: bool) -> &BatchState<T> { + pub fn processing_completed(&mut self, was_sucessful: bool, logger: &Logger) -> &BatchState<T> { match self.state.poison() { BatchState::Processing(attempt) => { self.state = if !was_sucessful { @@ -247,12 +272,17 @@ impl<T: EthSpec> BatchInfo<T> { }; &self.state } - other => unreachable!("Processing completed for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Procesing completed for batch in wrong state"; "state" => ?other); + self.state = other; + &self.state + } } } #[must_use = "Batch may have failed"] - pub fn validation_failed(&mut self) -> &BatchState<T> { + pub fn validation_failed(&mut self, logger: &Logger) -> &BatchState<T> { match self.state.poison() { BatchState::AwaitingValidation(attempt) => { self.failed_processing_attempts.push(attempt); @@ -267,7 +297,12 @@ impl<T: EthSpec> BatchInfo<T> { }; &self.state } - other => unreachable!("Validation failed for batch in wrong state: {:?}", other), + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + crit!(logger, "Validation failed for batch in wrong state"; "state" => ?other); + self.state = other; + &self.state + } } } } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 755f9e0e0..8ed21616d 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -30,7 +30,6 @@ const BATCH_BUFFER_SIZE: u8 = 5; #[derive(PartialEq)] #[must_use = "Should be checked, since a failed chain must be removed. A chain that requested being removed and continued is now in an inconsistent state"] - pub enum ProcessingResult { KeepChain, RemoveChain, @@ -168,7 +167,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .batches .get_mut(&id) .expect("registered batch exists") - .download_failed() + .download_failed(&self.log) { return ProcessingResult::RemoveChain; } @@ -229,18 +228,17 @@ impl<T: BeaconChainTypes> SyncingChain<T> { if let Some(block) = beacon_block { // This is not a stream termination, simply add the block to the request - batch.add_block(block); + batch.add_block(block, &self.log); ProcessingResult::KeepChain } else { // A stream termination has been sent. This batch has ended. Process a completed batch. // Remove the request from the peer's active batches - self.peers .get_mut(peer_id) .unwrap_or_else(|| panic!("Batch is registered for the peer")) .remove(&batch_id); - match batch.download_completed() { + match batch.download_completed(&self.log) { Ok(received) => { let awaiting_batches = batch_id.saturating_sub( self.optimistic_start @@ -254,9 +252,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { } self.process_completed_batches(network) } - Err((expected, received, state)) => { - warn!(self.log, "Batch received out of range blocks"; - "epoch" => batch_id, "expected" => expected, "received" => received); + Err(state) => { if let BatchState::Failed = state { return ProcessingResult::RemoveChain; } @@ -285,7 +281,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { // result callback. This is done, because an empty batch could end a chain and the logic // for removing chains and checking completion is in the callback. - let blocks = batch.start_processing(); + let blocks = batch.start_processing(&self.log); let process_id = ProcessId::RangeBatchId(self.id, batch_id); self.current_processing_batch = Some(batch_id); @@ -299,7 +295,6 @@ impl<T: BeaconChainTypes> SyncingChain<T> { // blocks to continue, and the chain is expecting a processing result that won't // arrive. To mitigate this, (fake) fail this processing so that the batch is // re-downloaded. - // TODO: needs better handling self.on_batch_process_result(network, batch_id, &BatchProcessResult::Failed(false)) } else { ProcessingResult::KeepChain @@ -337,25 +332,29 @@ impl<T: BeaconChainTypes> SyncingChain<T> { BatchState::Processing(_) | BatchState::AwaitingDownload | BatchState::Failed - | BatchState::Poisoned - | BatchState::AwaitingValidation(_) => { + | BatchState::Poisoned => { // these are all inconsistent states: - // - Processing -> `self.current_processing_batch` is Some - // - Failed -> non recoverable batch. For a optimistic batch, it should + // - Processing -> `self.current_processing_batch` is None + // - Failed -> non recoverable batch. For an optimistic batch, it should // have been removed // - Poisoned -> this is an intermediate state that should never be reached // - AwaitingDownload -> A recoverable failed batch should have been // re-requested. - // - AwaitingValidation -> If an optimistic batch is successfully processed - // it is no longer considered an optimistic candidate. If the batch was - // empty the chain rejects it; if it was non empty the chain is advanced - // to this point (so that the old optimistic batch is now the processing - // target) unreachable!( "Optimistic batch indicates inconsistent chain state: {:?}", state ) } + BatchState::AwaitingValidation(_) => { + // This is possible due to race conditions, and tho it would be considered + // an inconsistent state, the chain can continue. If an optimistic batch + // is successfully processed it is no longer considered an optimistic + // candidate. If the batch was empty the chain rejects it; if it was non + // empty the chain is advanced to this point (so that the old optimistic + // batch is now the processing target) + crit!(self.log, "Optimistic batch should never be Awaiting Validation"; "batch" => epoch); + None + } } } else { None @@ -385,7 +384,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { // re-requested. // - AwaitingValidation -> self.processing_target should have been moved // forward - // - Processing -> `self.current_processing_batch` is Some + // - Processing -> `self.current_processing_batch` is None // - Poisoned -> Intermediate state that should never be reached unreachable!( "Robust target batch indicates inconsistent chain state: {:?}", @@ -441,7 +440,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .batches .get_mut(&batch_id) .expect("Chain was expecting a known batch"); - let _ = batch.processing_completed(true); + let _ = batch.processing_completed(true, &self.log); // If the processed batch was not empty, we can validate previous unvalidated // blocks. if *was_non_empty { @@ -489,7 +488,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .expect("batch is processing blocks from a peer"); debug!(self.log, "Batch processing failed"; "imported_blocks" => imported_blocks, "batch_epoch" => batch_id, "peer" => %peer, "client" => %network.client_type(&peer)); - if let BatchState::Failed = batch.processing_completed(false) { + if let BatchState::Failed = batch.processing_completed(false, &self.log) { // check that we have not exceeded the re-process retry counter // If a batch has exceeded the invalid batch lookup attempts limit, it means // that it is likely all peers in this chain are are sending invalid batches @@ -566,6 +565,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { // safety check for batch boundaries if validating_epoch % EPOCHS_PER_BATCH != self.start_epoch % EPOCHS_PER_BATCH { crit!(self.log, "Validating Epoch is not aligned"); + return; } // batches in the range [BatchId, ..) (not yet validated) @@ -690,7 +690,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { let mut redownload_queue = Vec::new(); for (id, batch) in self.batches.range_mut(..batch_id) { - if let BatchState::Failed = batch.validation_failed() { + if let BatchState::Failed = batch.validation_failed(&self.log) { // remove the chain early return ProcessingResult::RemoveChain; } @@ -804,7 +804,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .get_mut(peer_id) .expect("Peer belongs to the chain") .remove(&batch_id); - if let BatchState::Failed = batch.download_failed() { + if let BatchState::Failed = batch.download_failed(&self.log) { return ProcessingResult::RemoveChain; } self.retry_batch_download(network, batch_id) @@ -859,7 +859,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { match network.blocks_by_range_request(peer.clone(), request, self.id, batch_id) { Ok(request_id) => { // inform the batch about the new request - batch.start_downloading_from_peer(peer.clone(), request_id); + batch.start_downloading_from_peer(peer.clone(), request_id, &self.log); if self .optimistic_start .map(|epoch| epoch == batch_id) @@ -881,12 +881,12 @@ impl<T: BeaconChainTypes> SyncingChain<T> { warn!(self.log, "Could not send batch request"; "batch_id" => batch_id, "error" => e, &batch); // register the failed download and check if the batch can be retried - batch.start_downloading_from_peer(peer.clone(), 1); // fake request_id is not relevant + batch.start_downloading_from_peer(peer.clone(), 1, &self.log); // fake request_id is not relevant self.peers .get_mut(&peer) .expect("peer belongs to the peer pool") .remove(&batch_id); - if let BatchState::Failed = batch.download_failed() { + if let BatchState::Failed = batch.download_failed(&self.log) { return ProcessingResult::RemoveChain; } else { return self.retry_batch_download(network, batch_id); From bcb629564ab2a567433f1d16dc0131b90e18d179 Mon Sep 17 00:00:00 2001 From: Age Manning <Age@AgeManning.com> Date: Mon, 5 Oct 2020 17:30:43 +1100 Subject: [PATCH 21/32] Improve error handling in network processing (#1654) * Improve error handling in network processing * Cargo fmt * Cargo fmt * Improve error handling for prior genesis * Remove dep --- Cargo.lock | 1 + beacon_node/network/src/router/processor.rs | 45 +++++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6665ed5ec..1ecfd872c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1612,6 +1612,7 @@ dependencies = [ "slog-async", "slog-stdlog", "slog-term", + "slot_clock", "smallvec 1.4.2", "snap", "tempdir", diff --git a/beacon_node/network/src/router/processor.rs b/beacon_node/network/src/router/processor.rs index 36b799c8d..ae6959437 100644 --- a/beacon_node/network/src/router/processor.rs +++ b/beacon_node/network/src/router/processor.rs @@ -3,13 +3,14 @@ use crate::beacon_processor::{ }; use crate::service::NetworkMessage; use crate::sync::{PeerSyncInfo, SyncMessage}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2_libp2p::rpc::*; use eth2_libp2p::{ MessageId, NetworkGlobals, PeerAction, PeerId, PeerRequestId, Request, Response, }; use itertools::process_results; use slog::{debug, error, o, trace, warn}; +use slot_clock::SlotClock; use std::cmp; use std::sync::Arc; use tokio::sync::mpsc; @@ -158,7 +159,9 @@ impl<T: BeaconChainTypes> Processor<T> { ); } - self.process_status(peer_id, status); + if let Err(e) = self.process_status(peer_id, status) { + error!(self.log, "Could not process status message"; "error" => format!("{:?}", e)); + } } /// Process a `Status` response from a peer. @@ -175,22 +178,29 @@ impl<T: BeaconChainTypes> Processor<T> { ); // Process the status message, without sending back another status. - self.process_status(peer_id, status); + if let Err(e) = self.process_status(peer_id, status) { + error!(self.log, "Could not process status message"; "error" => format!("{:?}", e)); + } } /// Process a `Status` message, requesting new blocks if appropriate. /// /// Disconnects the peer if required. - fn process_status(&mut self, peer_id: PeerId, status: StatusMessage) { + fn process_status( + &mut self, + peer_id: PeerId, + status: StatusMessage, + ) -> Result<(), BeaconChainError> { let remote = PeerSyncInfo::from(status); let local = match PeerSyncInfo::from_chain(&self.chain) { Some(local) => local, None => { - return error!( + error!( self.log, "Failed to get peer sync info"; "msg" => "likely due to head lock contention" - ) + ); + return Err(BeaconChainError::CannotAttestToFutureState); } }; @@ -209,7 +219,11 @@ impl<T: BeaconChainTypes> Processor<T> { self.network .goodbye_peer(peer_id, GoodbyeReason::IrrelevantNetwork); } else if remote.head_slot - > self.chain.slot().unwrap_or_else(|_| Slot::from(0u64)) + FUTURE_SLOT_TOLERANCE + > self + .chain + .slot() + .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()) + + FUTURE_SLOT_TOLERANCE { // Note: If the slot_clock cannot be read, this will not error. Other system // components will deal with an invalid slot clock error. @@ -230,8 +244,7 @@ impl<T: BeaconChainTypes> Processor<T> { && self .chain .root_at_slot(start_slot(remote.finalized_epoch)) - .map(|root_opt| root_opt != Some(remote.finalized_root)) - .unwrap_or_else(|_| false) + .map(|root_opt| root_opt != Some(remote.finalized_root))? { // The remotes finalized epoch is less than or greater than ours, but the block root is // different to the one in our chain. @@ -267,8 +280,7 @@ impl<T: BeaconChainTypes> Processor<T> { } else if self .chain .store - .item_exists::<SignedBeaconBlock<T::EthSpec>>(&remote.head_root) - .unwrap_or_else(|_| false) + .item_exists::<SignedBeaconBlock<T::EthSpec>>(&remote.head_root)? { debug!( self.log, "Peer with known chain found"; @@ -293,6 +305,8 @@ impl<T: BeaconChainTypes> Processor<T> { ); self.send_to_sync(SyncMessage::AddPeer(peer_id, remote)); } + + Ok(()) } /// Handle a `BlocksByRoot` request from the peer. @@ -438,6 +452,11 @@ impl<T: BeaconChainTypes> Processor<T> { } } + let current_slot = self + .chain + .slot() + .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); + if blocks_sent < (req.count as usize) { debug!( self.log, @@ -445,7 +464,7 @@ impl<T: BeaconChainTypes> Processor<T> { "peer" => peer_id.to_string(), "msg" => "Failed to return all requested blocks", "start_slot" => req.start_slot, - "current_slot" => self.chain.slot().unwrap_or_else(|_| Slot::from(0_u64)).as_u64(), + "current_slot" => current_slot, "requested" => req.count, "returned" => blocks_sent); } else { @@ -454,7 +473,7 @@ impl<T: BeaconChainTypes> Processor<T> { "Sending BlocksByRange Response"; "peer" => peer_id.to_string(), "start_slot" => req.start_slot, - "current_slot" => self.chain.slot().unwrap_or_else(|_| Slot::from(0_u64)).as_u64(), + "current_slot" => current_slot, "requested" => req.count, "returned" => blocks_sent); } From 240181e840f8dad4d5c2f313418d2dcb749e5766 Mon Sep 17 00:00:00 2001 From: Age Manning <Age@AgeManning.com> Date: Mon, 5 Oct 2020 18:45:54 +1100 Subject: [PATCH 22/32] Upgrade discovery and restructure task execution (#1693) * Initial rebase * Remove old code * Correct release tests * Rebase commit * Remove eth2-testnet dep on eth2libp2p * Remove crates lost in rebase * Remove unused dep --- Cargo.lock | 317 ++++++++++-------- Cargo.toml | 1 + beacon_node/Cargo.toml | 1 + beacon_node/beacon_chain/Cargo.toml | 3 +- beacon_node/beacon_chain/src/eth1_chain.rs | 2 +- beacon_node/client/Cargo.toml | 1 + beacon_node/client/src/notifier.rs | 2 +- beacon_node/eth1/Cargo.toml | 3 +- beacon_node/eth1/src/service.rs | 2 +- beacon_node/eth2_libp2p/Cargo.toml | 7 +- beacon_node/eth2_libp2p/src/config.rs | 1 + beacon_node/eth2_libp2p/src/discovery/enr.rs | 4 +- beacon_node/eth2_libp2p/src/discovery/mod.rs | 19 +- beacon_node/eth2_libp2p/src/service.rs | 4 +- beacon_node/eth2_libp2p/tests/common/mod.rs | 2 +- beacon_node/http_api/Cargo.toml | 2 - beacon_node/network/Cargo.toml | 2 +- .../network/src/beacon_processor/mod.rs | 2 +- beacon_node/network/src/router/mod.rs | 2 +- beacon_node/network/src/router/processor.rs | 2 +- beacon_node/network/src/service.rs | 4 +- beacon_node/network/src/service/tests.rs | 2 +- beacon_node/network/src/sync/manager.rs | 2 +- beacon_node/src/lib.rs | 13 +- beacon_node/timer/Cargo.toml | 2 +- beacon_node/timer/src/lib.rs | 2 +- beacon_node/websocket_server/Cargo.toml | 3 +- beacon_node/websocket_server/src/lib.rs | 2 +- boot_node/src/config.rs | 2 +- common/eth2_testnet_config/Cargo.toml | 2 +- common/task_executor/Cargo.toml | 13 + .../task_executor/src/lib.rs | 29 +- .../task_executor}/src/metrics.rs | 0 lighthouse/environment/Cargo.toml | 4 +- lighthouse/environment/src/lib.rs | 35 +- 35 files changed, 273 insertions(+), 221 deletions(-) create mode 100644 common/task_executor/Cargo.toml rename lighthouse/environment/src/executor.rs => common/task_executor/src/lib.rs (91%) rename {lighthouse/environment => common/task_executor}/src/metrics.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 1ecfd872c..183ffb6d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7001367fde4c768a19d1029f0a8be5abd9308e1119846d5bd9ad26297b8faf5" dependencies = [ - "aes-soft", - "aesni", - "block-cipher", + "aes-soft 0.4.0", + "aesni 0.7.0", + "block-cipher 0.7.1", +] + +[[package]] +name = "aes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2bc6d3f370b5666245ff421e231cba4353df936e26986d2918e61a8fd6aef6" +dependencies = [ + "aes-soft 0.5.0", + "aesni 0.8.0", + "block-cipher 0.8.0", ] [[package]] @@ -99,10 +110,10 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e60aeefd2a0243bd53a42e92444e039f67c3d7f0382c9813577696e7c10bf3" dependencies = [ - "aes-soft", - "aesni", + "aes-soft 0.4.0", + "aesni 0.7.0", "ctr", - "stream-cipher", + "stream-cipher 0.4.1", ] [[package]] @@ -112,8 +123,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f5007801316299f922a6198d1d09a0bae95786815d066d5880d13f7c45ead1" dependencies = [ "aead", - "aes", - "block-cipher", + "aes 0.4.0", + "block-cipher 0.7.1", + "ghash", + "subtle 2.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0301c9e9c443494d970a07885e8cf3e587bae8356a1d5abd0999068413f7205f" +dependencies = [ + "aead", + "aes 0.5.0", + "block-cipher 0.8.0", "ghash", "subtle 2.3.0", ] @@ -124,20 +148,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7" dependencies = [ - "block-cipher", + "block-cipher 0.7.1", "byteorder", "opaque-debug 0.2.3", ] +[[package]] +name = "aes-soft" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dd91889c49327ad7ef3b500fd1109dbd3c509a03db0d4a9ce413b79f575cb6" +dependencies = [ + "block-cipher 0.8.0", + "byteorder", + "opaque-debug 0.3.0", +] + [[package]] name = "aesni" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264" dependencies = [ - "block-cipher", + "block-cipher 0.7.1", "opaque-debug 0.2.3", - "stream-cipher", + "stream-cipher 0.4.1", +] + +[[package]] +name = "aesni" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6fe808308bb07d393e2ea47780043ec47683fcf19cf5efc8ca51c50cc8c68a" +dependencies = [ + "block-cipher 0.8.0", + "opaque-debug 0.3.0", ] [[package]] @@ -292,9 +337,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.50" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" +checksum = "ec1931848a574faa8f7c71a12ea00453ff5effbb5f51afe7f77d7a48cace6ac1" dependencies = [ "addr2line", "cfg-if", @@ -373,6 +418,7 @@ dependencies = [ "smallvec 1.4.2", "state_processing", "store", + "task_executor", "tempfile", "tokio 0.2.22", "tree_hash", @@ -410,6 +456,7 @@ dependencies = [ "slog-async", "slog-term", "store", + "task_executor", "tokio 0.2.22", "types", ] @@ -502,6 +549,15 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "block-cipher" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f337a3e6da609650eb74e02bc9fac7b735049f7623ab12f2e4c719316fcc7e80" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -715,36 +771,38 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chacha20" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c0f07ac275808b7bf9a39f2fd013aae1498be83632814c8c4e0bd53f2dc58" +checksum = "244fbce0d47e97e8ef2f63b81d5e05882cb518c68531eb33194990d7b7e85845" dependencies = [ - "stream-cipher", + "stream-cipher 0.7.1", "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18b0c90556d8e3fec7cf18d84a2f53d27b21288f2fe481b830fadcf809e48205" +checksum = "9bf18d374d66df0c05cdddd528a7db98f78c28e2519b120855c4f84c5027b1f5" dependencies = [ "aead", "chacha20", "poly1305", - "stream-cipher", + "stream-cipher 0.7.1", "zeroize", ] [[package]] name = "chrono" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", "time 0.1.44", + "winapi 0.3.9", ] [[package]] @@ -806,7 +864,8 @@ dependencies = [ "sloggers", "slot_clock", "store", - "time 0.2.21", + "task_executor", + "time 0.2.22", "timer", "tokio 0.2.22", "toml", @@ -1104,7 +1163,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3592740fd55aaf61dd72df96756bd0d11e6037b89dcf30ae2e1895b267692be" dependencies = [ - "stream-cipher", + "stream-cipher 0.4.1", ] [[package]] @@ -1117,19 +1176,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "curve25519-dalek" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d85653f070353a16313d0046f173f70d1aadd5b42600a14de626f0dfb3473a5" -dependencies = [ - "byteorder", - "digest 0.8.1", - "rand_core 0.5.1", - "subtle 2.3.0", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "3.0.0" @@ -1213,9 +1259,9 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.10" +version = "0.99.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dcfabdab475c16a93d669dddfc393027803e347d09663f524447f642fbb84ba" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2", "quote", @@ -1279,11 +1325,11 @@ checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" [[package]] name = "discv5" -version = "0.1.0-alpha.12" +version = "0.1.0-alpha.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65a5e4a22a4c1d7142f54ac068b8c6252610ed0ebf00264f39eccee7f88fe4b9" +checksum = "051e80f35af336a84e3960df036eea52366daee461f0b6ee2feee15c6d101718" dependencies = [ - "aes-gcm", + "aes-gcm 0.6.0", "arrayvec", "digest 0.8.1", "enr", @@ -1308,12 +1354,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dtoa" version = "0.4.6" @@ -1335,7 +1375,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek 3.0.0", + "curve25519-dalek", "ed25519", "rand 0.7.3", "serde", @@ -1383,9 +1423,9 @@ dependencies = [ [[package]] name = "enr" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3137b4854534673ea350751670c6fe53920394a328ba9ce4d9acabd4f60a586" +checksum = "7867d4637e09af7576d9399d02c45784daf264fe6bb0713496a53c51a9154e21" dependencies = [ "base64 0.12.3", "bs58", @@ -1396,7 +1436,7 @@ dependencies = [ "rand 0.7.3", "rlp", "serde", - "tiny-keccak 2.0.2", + "sha3", "zeroize", ] @@ -1418,13 +1458,10 @@ name = "environment" version = "0.1.2" dependencies = [ "ctrlc", - "discv5", "eth2_config", "eth2_testnet_config", "exit-future", "futures 0.3.5", - "lazy_static", - "lighthouse_metrics", "logging", "parking_lot 0.11.0", "slog", @@ -1432,6 +1469,7 @@ dependencies = [ "slog-json", "slog-term", "sloggers", + "task_executor", "tokio 0.2.22", "types", ] @@ -1468,6 +1506,7 @@ dependencies = [ "slog", "sloggers", "state_processing", + "task_executor", "tokio 0.2.22", "toml", "tree_hash", @@ -1587,7 +1626,6 @@ dependencies = [ "directory", "dirs", "discv5", - "environment", "error-chain", "eth2_ssz", "eth2_ssz_derive", @@ -1610,11 +1648,10 @@ dependencies = [ "sha2 0.9.1", "slog", "slog-async", - "slog-stdlog", "slog-term", - "slot_clock", "smallvec 1.4.2", "snap", + "task_executor", "tempdir", "tiny-keccak 2.0.2", "tokio 0.2.22", @@ -1806,9 +1843,9 @@ checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" [[package]] name = "flate2" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766d0e77a2c1502169d4a93ff3b8c15a71fd946cd0126309752104e5f3c46d94" +checksum = "da80be589a72651dcda34d8b35bcdc9b7254ad06325611074d9cc0fbb19f60ee" dependencies = [ "cfg-if", "crc32fast", @@ -2187,9 +2224,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "hashset_delay" @@ -2586,7 +2623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" dependencies = [ "autocfg 1.0.1", - "hashbrown 0.9.0", + "hashbrown 0.9.1", ] [[package]] @@ -2780,9 +2817,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.77" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" +checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743" [[package]] name = "libflate" @@ -2981,7 +3018,7 @@ version = "0.24.1" source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "bytes 0.5.6", - "curve25519-dalek 3.0.0", + "curve25519-dalek", "futures 0.3.5", "lazy_static", "libp2p-core 0.22.2", @@ -2992,7 +3029,7 @@ dependencies = [ "sha2 0.9.1", "snow", "static_assertions", - "x25519-dalek 1.1.0", + "x25519-dalek", "zeroize", ] @@ -3489,7 +3526,6 @@ name = "network" version = "0.2.0" dependencies = [ "beacon_chain", - "environment", "error-chain", "eth2_libp2p", "eth2_ssz", @@ -3517,6 +3553,7 @@ dependencies = [ "smallvec 1.4.2", "state_processing", "store", + "task_executor", "tempfile", "tokio 0.2.22", "tree_hash", @@ -3686,9 +3723,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-src" -version = "111.10.2+1.1.1g" +version = "111.11.0+1.1.1h" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a287fdb22e32b5b60624d4a5a7a02dbe82777f730ec0dbc42a0554326fef5a70" +checksum = "380fe324132bea01f45239fadfec9343adb044615f29930d039bec1ae7b9fa5b" dependencies = [ "cc", ] @@ -3889,18 +3926,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.23" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +checksum = "13fbdfd6bdee3dc9be46452f86af4a4072975899cf8592466668620bebfbcc17" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.23" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +checksum = "c82fb1329f632c3552cf352d14427d57a511b1cf41db93b3a7d77906a82dcc8e" dependencies = [ "proc-macro2", "quote", @@ -3909,9 +3946,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "e555d9e657502182ac97b539fb3dae8b79cda19e3e4f8ffb5e8de4f18df93c95" [[package]] name = "pin-utils" @@ -3951,18 +3988,18 @@ checksum = "b18befed8bc2b61abc79a457295e7e838417326da1586050b919414073977f19" [[package]] name = "poly1305" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b42192ab143ed7619bf888a7f9c6733a9a2153b218e2cd557cfdb52fbf9bb1" +checksum = "22ce46de8e53ee414ca4d02bfefac75d8c12fba948b76622a40b4be34dfce980" dependencies = [ "universal-hash", ] [[package]] name = "polyval" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a50142b55ab3ed0e9f68dfb3709f1d90d29da24e91033f28b96330643107dc" +checksum = "a5884790f1ce3553ad55fec37b5aaac5882e0e845a2612df744d6c85c9bf046c" dependencies = [ "cfg-if", "universal-hash", @@ -4001,9 +4038,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.21" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] @@ -4099,15 +4136,15 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb14183cc7f213ee2410067e1ceeadba2a7478a59432ff0747a335202798b1e2" +checksum = "6d147edb77bcccbfc81fabffdc7bd50c13e103b15ca1e27515fe40de69a5776b" [[package]] name = "psutil" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094d0f0f32f77f62cd7d137d9b9599ef257d5c1323b36b25746679de2806f547" +checksum = "7cdb732329774b8765346796abd1e896e9b3c86aae7f135bb1dda98c2c460f55" dependencies = [ "cfg-if", "darwin-libproc", @@ -4118,7 +4155,7 @@ dependencies = [ "num_cpus", "once_cell", "platforms", - "snafu", + "thiserror", "unescape", ] @@ -4382,9 +4419,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270" +checksum = "dcf6960dc9a5b4ee8d3e4c5787b4a112a8818e0290a42ff664ad60692fdf2032" dependencies = [ "autocfg 1.0.1", "crossbeam-deque", @@ -4526,9 +4563,9 @@ checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" [[package]] name = "rlp" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a7d3f9bed94764eac15b8f14af59fac420c236adaff743b7bcc88e265cb4345" +checksum = "1190dcc8c3a512f1eef5d09bb8c84c7f39e1054e174d1795482e18f5272f2e73" dependencies = [ "rustc-hex", ] @@ -4793,9 +4830,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4" dependencies = [ "itoa", "ryu", @@ -4933,9 +4970,9 @@ checksum = "29f060a7d147e33490ec10da418795238fd7545bba241504d6b31a409f2e6210" [[package]] name = "simple_logger" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13a53ed2efd04911c8280f2da7bf9abd350c931b86bc7f9f2386fbafbf525ff9" +checksum = "b36ca4371e647131759047d7a0ac5e41e11fd540e0a49c9e158b1b94193081a1" dependencies = [ "atty", "chrono", @@ -5111,27 +5148,6 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" -[[package]] -name = "snafu" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c4e6046e4691afe918fd1b603fd6e515bcda5388a1092a9edbada307d159f09" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7073448732a89f2f3e6581989106067f403d378faeafb4a50812eb814170d3e5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "snap" version = "1.0.1" @@ -5140,11 +5156,11 @@ checksum = "da73c8f77aebc0e40c300b93f0a5f1bece7a248a36eee287d4e095f35c7b7d6e" [[package]] name = "snow" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32bf8474159a95551661246cda4976e89356999e3cbfef36f493dacc3fae1e8e" +checksum = "795dd7aeeee24468e5a32661f6d27f7b5cbed802031b2d7640c7b10f8fb2dd50" dependencies = [ - "aes-gcm", + "aes-gcm 0.7.0", "blake2", "chacha20poly1305", "rand 0.7.3", @@ -5153,7 +5169,7 @@ dependencies = [ "rustc_version", "sha2 0.9.1", "subtle 2.3.0", - "x25519-dalek 0.6.0", + "x25519-dalek", ] [[package]] @@ -5321,7 +5337,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f8ed9974042b8c3672ff3030a69fcc03b74c47c3d1ecb7755e8a3626011e88" dependencies = [ - "block-cipher", + "block-cipher 0.7.1", + "generic-array 0.14.4", +] + +[[package]] +name = "stream-cipher" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80e15f898d8d8f25db24c253ea615cc14acf418ff307822995814e7d42cfa89" +dependencies = [ + "block-cipher 0.8.0", "generic-array 0.14.4", ] @@ -5363,9 +5389,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" +checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" dependencies = [ "proc-macro2", "quote", @@ -5396,6 +5422,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" +[[package]] +name = "task_executor" +version = "0.1.0" +dependencies = [ + "exit-future", + "futures 0.3.5", + "lazy_static", + "lighthouse_metrics", + "slog", + "tokio 0.2.22", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -5507,9 +5545,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c2e31fb28e2a9f01f5ed6901b066c1ba2333c04b64dc61254142bafcb3feb2c" +checksum = "55b7151c9065e80917fbf285d9a5d1432f60db41d170ccafc749a136b41a93af" dependencies = [ "const_fn", "libc", @@ -5522,9 +5560,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9b6e9f095bc105e183e3cd493d72579be3181ad4004fceb01adbe9eecab2d" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" dependencies = [ "proc-macro-hack", "time-macros-impl", @@ -5548,11 +5586,11 @@ name = "timer" version = "0.2.0" dependencies = [ "beacon_chain", - "environment", "futures 0.3.5", "parking_lot 0.11.0", "slog", "slot_clock", + "task_executor", "tokio 0.2.22", "types", ] @@ -5957,20 +5995,21 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" dependencies = [ "cfg-if", "log 0.4.11", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" dependencies = [ "lazy_static", ] @@ -6653,12 +6692,11 @@ dependencies = [ name = "websocket_server" version = "0.2.0" dependencies = [ - "environment", "futures 0.3.5", "serde", "serde_derive", - "serde_json", "slog", + "task_executor", "tokio 0.2.22", "types", "ws", @@ -6753,24 +6791,13 @@ dependencies = [ "winapi-build", ] -[[package]] -name = "x25519-dalek" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" -dependencies = [ - "curve25519-dalek 2.1.0", - "rand_core 0.5.1", - "zeroize", -] - [[package]] name = "x25519-dalek" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc614d95359fd7afc321b66d2107ede58b246b844cf5d8a0adcca413e439f088" dependencies = [ - "curve25519-dalek 3.0.0", + "curve25519-dalek", "rand_core 0.5.1", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index b8b2fdde7..d15b23be6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "common/slot_clock", "common/test_random_derive", "common/warp_utils", + "common/task_executor", "common/validator_dir", "consensus/cached_tree_hash", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index e1df8b49d..e1a8f4a14 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -33,6 +33,7 @@ logging = { path = "../common/logging" } directory = {path = "../common/directory"} futures = "0.3.5" environment = { path = "../lighthouse/environment" } +task_executor = { path = "../common/task_executor" } genesis = { path = "genesis" } eth2_testnet_config = { path = "../common/eth2_testnet_config" } eth2_libp2p = { path = "./eth2_libp2p" } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 481039c48..9ffff370e 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -12,6 +12,7 @@ participation_metrics = [] # Exposes validator participation metrics to Prometh [dev-dependencies] int_to_bytes = { path = "../../consensus/int_to_bytes" } maplit = "1.0.2" +environment = { path = "../../lighthouse/environment" } [dependencies] eth2_config = { path = "../../common/eth2_config" } @@ -54,7 +55,7 @@ bitvec = "0.17.4" bls = { path = "../../crypto/bls" } safe_arith = { path = "../../consensus/safe_arith" } fork_choice = { path = "../../consensus/fork_choice" } -environment = { path = "../../lighthouse/environment" } +task_executor = { path = "../../common/task_executor" } bus = "2.2.3" derivative = "2.1.1" itertools = "0.9.0" diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index b2477c3b1..ea455064e 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -1,5 +1,4 @@ use crate::metrics; -use environment::TaskExecutor; use eth1::{Config as Eth1Config, Eth1Block, Service as HttpService}; use eth2_hashing::hash; use slog::{debug, error, trace, Logger}; @@ -11,6 +10,7 @@ use std::collections::HashMap; use std::iter::DoubleEndedIterator; use std::marker::PhantomData; use store::{DBColumn, Error as StoreError, StoreItem}; +use task_executor::TaskExecutor; use types::{ BeaconState, BeaconStateError, ChainSpec, Deposit, Eth1Data, EthSpec, Hash256, Slot, Unsigned, DEPOSIT_TREE_DEPTH, diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 55742f851..16a773396 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -34,6 +34,7 @@ reqwest = { version = "0.10.4", features = ["native-tls-vendored"] } url = "2.1.1" eth1 = { path = "../eth1" } genesis = { path = "../genesis" } +task_executor = { path = "../../common/task_executor" } environment = { path = "../../lighthouse/environment" } eth2_ssz = "0.1.2" lazy_static = "1.4.0" diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index f82c9971d..523779687 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -22,7 +22,7 @@ const SPEEDO_OBSERVATIONS: usize = 4; /// Spawns a notifier service which periodically logs information about the node. pub fn spawn_notifier<T: BeaconChainTypes>( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, beacon_chain: Arc<BeaconChain<T>>, network: Arc<NetworkGlobals<T::EthSpec>>, milliseconds_per_slot: u64, diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 37eea2412..217444ff6 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -9,6 +9,7 @@ eth1_test_rig = { path = "../../testing/eth1_test_rig" } toml = "0.5.6" web3 = "0.11.0" sloggers = "1.0.0" +environment = { path = "../../lighthouse/environment" } [dependencies] reqwest = { version = "0.10.4", features = ["native-tls-vendored"] } @@ -29,4 +30,4 @@ state_processing = { path = "../../consensus/state_processing" } libflate = "1.0.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics"} lazy_static = "1.4.0" -environment = { path = "../../lighthouse/environment" } +task_executor = { path = "../../common/task_executor" } diff --git a/beacon_node/eth1/src/service.rs b/beacon_node/eth1/src/service.rs index ee203b645..6b6d65855 100644 --- a/beacon_node/eth1/src/service.rs +++ b/beacon_node/eth1/src/service.rs @@ -345,7 +345,7 @@ impl Service { /// - Err(_) if there is an error. /// /// Emits logs for debugging and errors. - pub fn auto_update(self, handle: environment::TaskExecutor) { + pub fn auto_update(self, handle: task_executor::TaskExecutor) { let update_interval = Duration::from_millis(self.config().auto_update_interval_millis); let mut interval = interval_at(Instant::now(), update_interval); diff --git a/beacon_node/eth2_libp2p/Cargo.toml b/beacon_node/eth2_libp2p/Cargo.toml index 5241208a9..bb2c88847 100644 --- a/beacon_node/eth2_libp2p/Cargo.toml +++ b/beacon_node/eth2_libp2p/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"] edition = "2018" [dependencies] -hex = "0.4.2" +discv5 = { version = "0.1.0-alpha.13", features = ["libp2p"] } types = { path = "../../consensus/types" } hashset_delay = { path = "../../common/hashset_delay" } eth2_ssz_types = { path = "../../consensus/ssz_types" } @@ -30,11 +30,11 @@ sha2 = "0.9.1" base64 = "0.12.1" snap = "1.0.0" void = "1.0.2" +hex = "0.4.2" tokio-io-timeout = "0.4.0" tokio-util = { version = "0.3.1", features = ["codec", "compat"] } -discv5 = { version = "0.1.0-alpha.12", features = ["libp2p"] } tiny-keccak = "2.0.2" -environment = { path = "../../lighthouse/environment" } +task_executor = { path = "../../common/task_executor" } rand = "0.7.3" directory = { path = "../../common/directory" } regex = "1.3.9" @@ -48,7 +48,6 @@ features = ["websocket", "identify", "mplex", "noise", "gossipsub", "dns", "tcp- [dev-dependencies] tokio = { version = "0.2.22", features = ["full"] } -slog-stdlog = "4.0.0" slog-term = "2.5.0" slog-async = "2.5.0" tempdir = "0.3.7" diff --git a/beacon_node/eth2_libp2p/src/config.rs b/beacon_node/eth2_libp2p/src/config.rs index f3fbbda53..93e0a423c 100644 --- a/beacon_node/eth2_libp2p/src/config.rs +++ b/beacon_node/eth2_libp2p/src/config.rs @@ -123,6 +123,7 @@ impl Default for Config { .request_retries(1) .enr_peer_update_min(10) .query_parallelism(5) + .disable_report_discovered_peers() .query_timeout(Duration::from_secs(30)) .query_peer_timeout(Duration::from_secs(2)) .ip_limit() // limits /24 IP's in buckets. diff --git a/beacon_node/eth2_libp2p/src/discovery/enr.rs b/beacon_node/eth2_libp2p/src/discovery/enr.rs index 6af9f21fb..853ea5f9a 100644 --- a/beacon_node/eth2_libp2p/src/discovery/enr.rs +++ b/beacon_node/eth2_libp2p/src/discovery/enr.rs @@ -144,12 +144,12 @@ pub fn build_enr<T: EthSpec>( let mut builder = create_enr_builder_from_config(config); // set the `eth2` field on our ENR - builder.add_value(ETH2_ENR_KEY.into(), enr_fork_id.as_ssz_bytes()); + builder.add_value(ETH2_ENR_KEY, &enr_fork_id.as_ssz_bytes()); // set the "attnets" field on our ENR let bitfield = BitVector::<T::SubnetBitfieldLength>::new(); - builder.add_value(BITFIELD_ENR_KEY.into(), bitfield.as_ssz_bytes()); + builder.add_value(BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes()); builder .build(enr_key) diff --git a/beacon_node/eth2_libp2p/src/discovery/mod.rs b/beacon_node/eth2_libp2p/src/discovery/mod.rs index b065265da..8c29d844c 100644 --- a/beacon_node/eth2_libp2p/src/discovery/mod.rs +++ b/beacon_node/eth2_libp2p/src/discovery/mod.rs @@ -365,7 +365,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> { /// If the external address needs to be modified, use `update_enr_udp_socket. pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { self.discv5 - .enr_insert("tcp", port.to_be_bytes().into()) + .enr_insert("tcp", &port.to_be_bytes()) .map_err(|e| format!("{:?}", e))?; // replace the global version @@ -383,18 +383,18 @@ impl<TSpec: EthSpec> Discovery<TSpec> { match socket_addr { SocketAddr::V4(socket) => { self.discv5 - .enr_insert("ip", socket.ip().octets().into()) + .enr_insert("ip", &socket.ip().octets()) .map_err(|e| format!("{:?}", e))?; self.discv5 - .enr_insert("udp", socket.port().to_be_bytes().into()) + .enr_insert("udp", &socket.port().to_be_bytes()) .map_err(|e| format!("{:?}", e))?; } SocketAddr::V6(socket) => { self.discv5 - .enr_insert("ip6", socket.ip().octets().into()) + .enr_insert("ip6", &socket.ip().octets()) .map_err(|e| format!("{:?}", e))?; self.discv5 - .enr_insert("udp6", socket.port().to_be_bytes().into()) + .enr_insert("udp6", &socket.port().to_be_bytes()) .map_err(|e| format!("{:?}", e))?; } } @@ -439,7 +439,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> { // insert the bitfield into the ENR record self.discv5 - .enr_insert(BITFIELD_ENR_KEY, current_bitfield.as_ssz_bytes()) + .enr_insert(BITFIELD_ENR_KEY, ¤t_bitfield.as_ssz_bytes()) .map_err(|e| format!("{:?}", e))?; // replace the global version @@ -468,7 +468,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> { let _ = self .discv5 - .enr_insert(ETH2_ENR_KEY, enr_fork_id.as_ssz_bytes()) + .enr_insert(ETH2_ENR_KEY, &enr_fork_id.as_ssz_bytes()) .map_err(|e| { warn!( self.log, @@ -858,7 +858,10 @@ impl<TSpec: EthSpec> Discovery<TSpec> { // Still awaiting the event stream, poll it if let Poll::Ready(event_stream) = fut.poll_unpin(cx) { match event_stream { - Ok(stream) => self.event_stream = EventStream::Present(stream), + Ok(stream) => { + debug!(self.log, "Discv5 event stream ready"); + self.event_stream = EventStream::Present(stream); + } Err(e) => { slog::crit!(self.log, "Discv5 event stream failed"; "error" => e.to_string()); self.event_stream = EventStream::InActive; diff --git a/beacon_node/eth2_libp2p/src/service.rs b/beacon_node/eth2_libp2p/src/service.rs index 52286c05d..ece4cd1d5 100644 --- a/beacon_node/eth2_libp2p/src/service.rs +++ b/beacon_node/eth2_libp2p/src/service.rs @@ -59,7 +59,7 @@ pub struct Service<TSpec: EthSpec> { impl<TSpec: EthSpec> Service<TSpec> { pub async fn new( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, config: &NetworkConfig, enr_fork_id: EnrForkId, log: &slog::Logger, @@ -109,7 +109,7 @@ impl<TSpec: EthSpec> Service<TSpec> { Behaviour::new(&local_keypair, config, network_globals.clone(), &log).await?; // use the executor for libp2p - struct Executor(environment::TaskExecutor); + struct Executor(task_executor::TaskExecutor); impl libp2p::core::Executor for Executor { fn exec(&self, f: Pin<Box<dyn Future<Output = ()> + Send>>) { self.0.spawn(f, "libp2p"); diff --git a/beacon_node/eth2_libp2p/tests/common/mod.rs b/beacon_node/eth2_libp2p/tests/common/mod.rs index dc81cdb68..916f2f841 100644 --- a/beacon_node/eth2_libp2p/tests/common/mod.rs +++ b/beacon_node/eth2_libp2p/tests/common/mod.rs @@ -99,7 +99,7 @@ pub async fn build_libp2p_instance(boot_nodes: Vec<Enr>, log: slog::Logger) -> L let (signal, exit) = exit_future::signal(); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = environment::TaskExecutor::new( + let executor = task_executor::TaskExecutor::new( tokio::runtime::Handle::current(), exit, log.clone(), diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index a45ac5fec..0fd2ca5cb 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] warp = "0.2.5" serde = { version = "1.0.110", features = ["derive"] } diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 2809fd792..2bb2a94ea 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -35,7 +35,7 @@ fnv = "1.0.6" rlp = "0.4.5" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -environment = { path = "../../lighthouse/environment" } +task_executor = { path = "../../common/task_executor" } igd = "0.11.1" itertools = "0.9.0" num_cpus = "1.13.0" diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/network/src/beacon_processor/mod.rs index 18983b620..b23b40e54 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/network/src/beacon_processor/mod.rs @@ -37,12 +37,12 @@ use crate::{metrics, service::NetworkMessage, sync::SyncMessage}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError}; -use environment::TaskExecutor; use eth2_libp2p::{MessageId, NetworkGlobals, PeerId}; use slog::{crit, debug, error, trace, warn, Logger}; use std::collections::VecDeque; use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; +use task_executor::TaskExecutor; use tokio::sync::{mpsc, oneshot}; use types::{ Attestation, AttesterSlashing, EthSpec, Hash256, ProposerSlashing, SignedAggregateAndProof, diff --git a/beacon_node/network/src/router/mod.rs b/beacon_node/network/src/router/mod.rs index 0fa2494ff..4701bdb73 100644 --- a/beacon_node/network/src/router/mod.rs +++ b/beacon_node/network/src/router/mod.rs @@ -74,7 +74,7 @@ impl<T: BeaconChainTypes> Router<T> { beacon_chain: Arc<BeaconChain<T>>, network_globals: Arc<NetworkGlobals<T::EthSpec>>, network_send: mpsc::UnboundedSender<NetworkMessage<T::EthSpec>>, - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, log: slog::Logger, ) -> error::Result<mpsc::UnboundedSender<RouterMessage<T::EthSpec>>> { let message_handler_log = log.new(o!("service"=> "router")); diff --git a/beacon_node/network/src/router/processor.rs b/beacon_node/network/src/router/processor.rs index ae6959437..266b1d45d 100644 --- a/beacon_node/network/src/router/processor.rs +++ b/beacon_node/network/src/router/processor.rs @@ -41,7 +41,7 @@ pub struct Processor<T: BeaconChainTypes> { impl<T: BeaconChainTypes> Processor<T> { /// Instantiate a `Processor` instance pub fn new( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, beacon_chain: Arc<BeaconChain<T>>, network_globals: Arc<NetworkGlobals<T::EthSpec>>, network_send: mpsc::UnboundedSender<NetworkMessage<T::EthSpec>>, diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 9f5fbf355..ec86696b3 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -121,7 +121,7 @@ impl<T: BeaconChainTypes> NetworkService<T> { pub async fn start( beacon_chain: Arc<BeaconChain<T>>, config: &NetworkConfig, - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, ) -> error::Result<( Arc<NetworkGlobals<T::EthSpec>>, mpsc::UnboundedSender<NetworkMessage<T::EthSpec>>, @@ -207,7 +207,7 @@ impl<T: BeaconChainTypes> NetworkService<T> { } fn spawn_service<T: BeaconChainTypes>( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, mut service: NetworkService<T>, ) -> error::Result<()> { let mut exit_rx = executor.exit(); diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index 2efcf889e..9888dd784 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -41,7 +41,7 @@ mod tests { let (signal, exit) = exit_future::signal(); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = environment::TaskExecutor::new( + let executor = task_executor::TaskExecutor::new( runtime.handle().clone(), exit, log.clone(), diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index deedc1448..8f4d15c5c 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -205,7 +205,7 @@ impl SingleBlockRequest { /// chain. This allows the chain to be /// dropped during the syncing process which will gracefully end the `SyncManager`. pub fn spawn<T: BeaconChainTypes>( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, beacon_chain: Arc<BeaconChain<T>>, network_globals: Arc<NetworkGlobals<T::EthSpec>>, network_send: mpsc::UnboundedSender<NetworkMessage<T::EthSpec>>, diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index feff1e320..86592cfc7 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -122,7 +122,8 @@ impl<E: EthSpec> ProductionBeaconNode<E> { .tee_event_handler(client_config.websocket_server.clone())?; // Inject the executor into the discv5 network config. - client_config.network.discv5_config.executor = Some(Box::new(executor)); + let discv5_executor = Discv5Executor(executor); + client_config.network.discv5_config.executor = Some(Box::new(discv5_executor)); builder .build_beacon_chain()? @@ -153,3 +154,13 @@ impl<E: EthSpec> DerefMut for ProductionBeaconNode<E> { &mut self.0 } } + +// Implements the Discv5 Executor trait over our global executor +#[derive(Clone)] +struct Discv5Executor(task_executor::TaskExecutor); + +impl eth2_libp2p::discv5::Executor for Discv5Executor { + fn spawn(&self, future: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>) { + self.0.spawn(future, "discv5") + } +} diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index 2cee2c5de..6388a7ad8 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -12,4 +12,4 @@ tokio = { version = "0.2.22", features = ["full"] } slog = "2.5.2" parking_lot = "0.11.0" futures = "0.3.5" -environment = { path = "../../lighthouse/environment" } +task_executor = { path = "../../common/task_executor" } diff --git a/beacon_node/timer/src/lib.rs b/beacon_node/timer/src/lib.rs index 67aca9c27..74c9e5eb0 100644 --- a/beacon_node/timer/src/lib.rs +++ b/beacon_node/timer/src/lib.rs @@ -12,7 +12,7 @@ use tokio::time::{interval_at, Instant}; /// Spawns a timer service which periodically executes tasks for the beacon chain pub fn spawn_timer<T: BeaconChainTypes>( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, beacon_chain: Arc<BeaconChain<T>>, milliseconds_per_slot: u64, ) -> Result<(), &'static str> { diff --git a/beacon_node/websocket_server/Cargo.toml b/beacon_node/websocket_server/Cargo.toml index 00aa24973..902eb588b 100644 --- a/beacon_node/websocket_server/Cargo.toml +++ b/beacon_node/websocket_server/Cargo.toml @@ -10,9 +10,8 @@ edition = "2018" futures = "0.3.5" serde = "1.0.110" serde_derive = "1.0.110" -serde_json = "1.0.52" slog = "2.5.2" tokio = { version = "0.2.22", features = ["full"] } types = { path = "../../consensus/types" } ws = "0.9.1" -environment = { path = "../../lighthouse/environment" } +task_executor = { path = "../../common/task_executor" } diff --git a/beacon_node/websocket_server/src/lib.rs b/beacon_node/websocket_server/src/lib.rs index f9ed3e97e..1eea57ae2 100644 --- a/beacon_node/websocket_server/src/lib.rs +++ b/beacon_node/websocket_server/src/lib.rs @@ -34,7 +34,7 @@ impl<T: EthSpec> WebSocketSender<T> { } pub fn start_server<T: EthSpec>( - executor: environment::TaskExecutor, + executor: task_executor::TaskExecutor, config: &Config, ) -> Result<(WebSocketSender<T>, SocketAddr), String> { let log = executor.log(); diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index 2a4bba51d..edc3e3b21 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -103,7 +103,7 @@ impl<T: EthSpec> TryFrom<&ArgMatches<'_>> for BootNodeConfig<T> { ); // add to the local_enr - if let Err(e) = local_enr.insert("eth2", enr_fork.as_ssz_bytes(), &local_key) { + if let Err(e) = local_enr.insert("eth2", &enr_fork.as_ssz_bytes(), &local_key) { slog::warn!(logger, "Could not update eth2 field"; "error" => ?e); } } else { diff --git a/common/eth2_testnet_config/Cargo.toml b/common/eth2_testnet_config/Cargo.toml index f5351875c..dceb5bec7 100644 --- a/common/eth2_testnet_config/Cargo.toml +++ b/common/eth2_testnet_config/Cargo.toml @@ -17,6 +17,6 @@ tempdir = "0.3.7" serde = "1.0.110" serde_yaml = "0.8.11" types = { path = "../../consensus/types"} -enr = { version = "0.1.0", features = ["libsecp256k1", "ed25519"] } eth2_ssz = "0.1.2" eth2_config = { path = "../eth2_config"} +enr = "0.3.0" diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml new file mode 100644 index 000000000..ec0f2cfbf --- /dev/null +++ b/common/task_executor/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "task_executor" +version = "0.1.0" +authors = ["Sigma Prime <contact@sigmaprime.io>"] +edition = "2018" + +[dependencies] +tokio = "0.2.22" +slog = "2.5.2" +futures = "0.3.5" +exit-future = "0.2.0" +lazy_static = "1.4.0" +lighthouse_metrics = { path = "../lighthouse_metrics" } diff --git a/lighthouse/environment/src/executor.rs b/common/task_executor/src/lib.rs similarity index 91% rename from lighthouse/environment/src/executor.rs rename to common/task_executor/src/lib.rs index b5f415187..9f819dd2a 100644 --- a/lighthouse/environment/src/executor.rs +++ b/common/task_executor/src/lib.rs @@ -1,23 +1,24 @@ -use crate::metrics; +mod metrics; + use futures::channel::mpsc::Sender; use futures::prelude::*; -use slog::{debug, trace}; +use slog::{debug, o, trace}; use tokio::runtime::Handle; /// A wrapper over a runtime handle which can spawn async and blocking tasks. #[derive(Clone)] pub struct TaskExecutor { /// The handle to the runtime on which tasks are spawned - pub handle: Handle, + handle: Handle, /// The receiver exit future which on receiving shuts down the task - pub(crate) exit: exit_future::Exit, + exit: exit_future::Exit, /// Sender given to tasks, so that if they encounter a state in which execution cannot /// continue they can request that everything shuts down. /// /// The task must provide a reason for shutting down. - pub(crate) signal_tx: Sender<&'static str>, + signal_tx: Sender<&'static str>, - pub(crate) log: slog::Logger, + log: slog::Logger, } impl TaskExecutor { @@ -39,6 +40,16 @@ impl TaskExecutor { } } + /// Clones the task executor adding a service name. + pub fn clone_with_name(&self, service_name: String) -> Self { + TaskExecutor { + handle: self.handle.clone(), + exit: self.exit.clone(), + signal_tx: self.signal_tx.clone(), + log: self.log.new(o!("service" => service_name)), + } + } + /// Spawn a future on the tokio runtime wrapped in an `exit_future::Exit`. The task is canceled /// when the corresponding exit_future `Signal` is fired/dropped. /// @@ -148,9 +159,3 @@ impl TaskExecutor { &self.log } } - -impl discv5::Executor for TaskExecutor { - fn spawn(&self, future: std::pin::Pin<Box<dyn Future<Output = ()> + Send>>) { - self.spawn(future, "discv5") - } -} diff --git a/lighthouse/environment/src/metrics.rs b/common/task_executor/src/metrics.rs similarity index 100% rename from lighthouse/environment/src/metrics.rs rename to common/task_executor/src/metrics.rs diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 54a1f1f18..9cb8eeaab 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -10,6 +10,7 @@ slog = { version = "2.5.2", features = ["max_level_trace"] } sloggers = "1.0.0" types = { "path" = "../../consensus/types" } eth2_config = { "path" = "../../common/eth2_config" } +task_executor = { "path" = "../../common/task_executor" } eth2_testnet_config = { path = "../../common/eth2_testnet_config" } logging = { path = "../../common/logging" } slog-term = "2.5.0" @@ -19,6 +20,3 @@ futures = "0.3.5" parking_lot = "0.11.0" slog-json = "2.3.0" exit-future = "0.2.0" -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -discv5 = { version = "0.1.0-alpha.12", features = ["libp2p"] } diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index c175e6cab..a58930842 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -15,7 +15,6 @@ use futures::channel::{ }; use futures::{future, StreamExt}; -pub use executor::TaskExecutor; use slog::{info, o, Drain, Level, Logger}; use sloggers::{null::NullLoggerBuilder, Build}; use std::cell::RefCell; @@ -23,10 +22,9 @@ use std::ffi::OsStr; use std::fs::{rename as FsRename, OpenOptions}; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; +use task_executor::TaskExecutor; use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; use types::{EthSpec, InteropEthSpec, MainnetEthSpec, MinimalEthSpec}; -mod executor; -mod metrics; pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; const LOG_CHANNEL_SIZE: usize = 2048; @@ -311,12 +309,7 @@ impl<E: EthSpec> RuntimeContext<E> { /// The generated service will have the `service_name` in all it's logs. pub fn service_context(&self, service_name: String) -> Self { Self { - executor: TaskExecutor { - handle: self.executor.handle.clone(), - signal_tx: self.executor.signal_tx.clone(), - exit: self.executor.exit.clone(), - log: self.executor.log.new(o!("service" => service_name)), - }, + executor: self.executor.clone_with_name(service_name), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), } @@ -361,12 +354,12 @@ impl<E: EthSpec> Environment<E> { /// Returns a `Context` where no "service" has been added to the logger output. pub fn core_context(&mut self) -> RuntimeContext<E> { RuntimeContext { - executor: TaskExecutor { - exit: self.exit.clone(), - signal_tx: self.signal_tx.clone(), - handle: self.runtime().handle().clone(), - log: self.log.clone(), - }, + executor: TaskExecutor::new( + self.runtime().handle().clone(), + self.exit.clone(), + self.log.clone(), + self.signal_tx.clone(), + ), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), } @@ -375,12 +368,12 @@ impl<E: EthSpec> Environment<E> { /// Returns a `Context` where the `service_name` is added to the logger output. pub fn service_context(&mut self, service_name: String) -> RuntimeContext<E> { RuntimeContext { - executor: TaskExecutor { - exit: self.exit.clone(), - signal_tx: self.signal_tx.clone(), - handle: self.runtime().handle().clone(), - log: self.log.new(o!("service" => service_name)), - }, + executor: TaskExecutor::new( + self.runtime().handle().clone(), + self.exit.clone(), + self.log.new(o!("service" => service_name)), + self.signal_tx.clone(), + ), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), } From ee7c8a0b7e20a01ab0ac0946de22d4d7cece0564 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Mon, 5 Oct 2020 08:22:19 +0000 Subject: [PATCH 23/32] Update external deps (#1711) ## Issue Addressed - Resolves #1706 ## Proposed Changes Updates dependencies across the workspace. Any crate that was not able to be brought to the latest version is listed in #1712. ## Additional Info NA --- Cargo.lock | 249 ++++++++++-------- account_manager/Cargo.toml | 12 +- beacon_node/Cargo.toml | 12 +- beacon_node/beacon_chain/Cargo.toml | 24 +- beacon_node/client/Cargo.toml | 20 +- beacon_node/eth1/Cargo.toml | 12 +- beacon_node/eth2_libp2p/Cargo.toml | 18 +- beacon_node/genesis/Cargo.toml | 8 +- beacon_node/http_api/Cargo.toml | 6 +- beacon_node/http_metrics/Cargo.toml | 6 +- beacon_node/network/Cargo.toml | 12 +- beacon_node/operation_pool/Cargo.toml | 4 +- beacon_node/store/Cargo.toml | 14 +- beacon_node/websocket_server/Cargo.toml | 4 +- boot_node/Cargo.toml | 6 +- common/account_utils/Cargo.toml | 10 +- common/clap_utils/Cargo.toml | 4 +- common/compare_fields_derive/Cargo.toml | 4 +- common/deposit_contract/Cargo.toml | 6 +- common/directory/Cargo.toml | 4 +- common/eth2/Cargo.toml | 8 +- common/eth2_config/Cargo.toml | 4 +- common/eth2_interop_keypairs/Cargo.toml | 8 +- common/eth2_testnet_config/Cargo.toml | 6 +- common/lighthouse_metrics/Cargo.toml | 2 +- common/logging/Cargo.toml | 2 +- common/test_random_derive/Cargo.toml | 4 +- common/validator_dir/Cargo.toml | 6 +- common/warp_utils/Cargo.toml | 4 +- consensus/cached_tree_hash/Cargo.toml | 6 +- consensus/fork_choice/Cargo.toml | 6 +- consensus/int_to_bytes/Cargo.toml | 4 +- consensus/merkle_proof/Cargo.toml | 2 +- consensus/proto_array/Cargo.toml | 6 +- consensus/serde_utils/Cargo.toml | 6 +- consensus/ssz/Cargo.toml | 4 +- consensus/ssz_derive/Cargo.toml | 4 +- consensus/ssz_types/Cargo.toml | 8 +- consensus/state_processing/Cargo.toml | 18 +- consensus/swap_or_not_shuffle/Cargo.toml | 4 +- consensus/tree_hash/Cargo.toml | 6 +- consensus/tree_hash_derive/Cargo.toml | 4 +- consensus/types/Cargo.toml | 22 +- crypto/bls/Cargo.toml | 16 +- crypto/eth2_hashing/Cargo.toml | 2 +- crypto/eth2_key_derivation/Cargo.toml | 6 +- crypto/eth2_keystore/Cargo.toml | 24 +- crypto/eth2_wallet/Cargo.toml | 10 +- lcli/Cargo.toml | 18 +- lighthouse/Cargo.toml | 6 +- lighthouse/environment/Cargo.toml | 6 +- testing/ef_tests/Cargo.toml | 14 +- testing/eth1_test_rig/Cargo.toml | 2 +- testing/node_test_rig/Cargo.toml | 4 +- testing/simulator/Cargo.toml | 4 +- validator_client/Cargo.toml | 20 +- .../slashing_protection/Cargo.toml | 16 +- 57 files changed, 385 insertions(+), 342 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 183ffb6d3..9a44c5f55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ dependencies = [ "clap_utils", "deposit_contract", "directory", - "dirs", + "dirs 3.0.1", "environment", "eth2_keystore", "eth2_ssz", @@ -106,14 +106,14 @@ dependencies = [ [[package]] name = "aes-ctr" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e60aeefd2a0243bd53a42e92444e039f67c3d7f0382c9813577696e7c10bf3" +checksum = "64c3b03608ea1c077228520a167cca2514dc7cd8100a81b30a2b38be985234e5" dependencies = [ - "aes-soft 0.4.0", - "aesni 0.7.0", + "aes-soft 0.5.0", + "aesni 0.9.0", "ctr", - "stream-cipher 0.4.1", + "stream-cipher", ] [[package]] @@ -172,7 +172,6 @@ checksum = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264" dependencies = [ "block-cipher 0.7.1", "opaque-debug 0.2.3", - "stream-cipher 0.4.1", ] [[package]] @@ -186,14 +185,22 @@ dependencies = [ ] [[package]] -name = "ahash" -version = "0.2.18" +name = "aesni" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3" +checksum = "b6a4d655ae633a96d0acaf0fd7e76aafb8ca5732739bba37aac6f882c8fce656" dependencies = [ - "const-random", + "block-cipher 0.8.0", + "opaque-debug 0.3.0", + "stream-cipher", ] +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" + [[package]] name = "aho-corasick" version = "0.7.13" @@ -344,7 +351,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.4.2", "object", "rustc-demangle", ] @@ -371,11 +378,17 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "beacon_chain" version = "0.2.0" dependencies = [ - "bitvec", + "bitvec 0.19.3", "bls", "bus", "derivative", @@ -436,7 +449,7 @@ dependencies = [ "client", "ctrlc", "directory", - "dirs", + "dirs 3.0.1", "environment", "eth2_config", "eth2_libp2p", @@ -480,7 +493,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" dependencies = [ "either", - "radium", + "radium 0.3.0", +] + +[[package]] +name = "bitvec" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11593270830d9b037fbead730bb0c05ef6fbf6be55537a1e8e5892edef7e1f03" +dependencies = [ + "funty", + "radium 0.5.1", + "tap", + "wyz", ] [[package]] @@ -582,7 +607,7 @@ dependencies = [ "eth2_hashing", "eth2_ssz", "ethereum-types", - "hex 0.3.2", + "hex 0.4.2", "milagro_bls", "rand 0.7.3", "serde", @@ -775,7 +800,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "244fbce0d47e97e8ef2f63b81d5e05882cb518c68531eb33194990d7b7e85845" dependencies = [ - "stream-cipher 0.7.1", + "stream-cipher", "zeroize", ] @@ -788,7 +813,7 @@ dependencies = [ "aead", "chacha20", "poly1305", - "stream-cipher 0.7.1", + "stream-cipher", "zeroize", ] @@ -825,7 +850,7 @@ name = "clap_utils" version = "0.1.0" dependencies = [ "clap", - "dirs", + "dirs 3.0.1", "eth2_ssz", "eth2_testnet_config", "hex 0.4.2", @@ -839,7 +864,7 @@ dependencies = [ "beacon_chain", "bus", "directory", - "dirs", + "dirs 3.0.1", "environment", "error-chain", "eth1", @@ -938,26 +963,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "const-random" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" -dependencies = [ - "getrandom", - "proc-macro-hack", -] - [[package]] name = "const_fn" version = "0.4.2" @@ -1135,6 +1140,16 @@ dependencies = [ "subtle 2.3.0", ] +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array 0.14.4", + "subtle 2.3.0", +] + [[package]] name = "csv" version = "1.1.3" @@ -1159,11 +1174,11 @@ dependencies = [ [[package]] name = "ctr" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3592740fd55aaf61dd72df96756bd0d11e6037b89dcf30ae2e1895b267692be" +checksum = "cc03dee3a2843ac6eb4b5fb39cfcf4cb034d078555d1f4a0afbed418b822f3c2" dependencies = [ - "stream-cipher 0.4.1", + "stream-cipher", ] [[package]] @@ -1292,7 +1307,7 @@ version = "0.1.0" dependencies = [ "clap", "clap_utils", - "dirs", + "dirs 3.0.1", "eth2_testnet_config", ] @@ -1306,6 +1321,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.3.5" @@ -1573,7 +1597,7 @@ dependencies = [ name = "eth2_interop_keypairs" version = "0.2.0" dependencies = [ - "base64 0.12.3", + "base64 0.13.0", "bls", "eth2_hashing", "hex 0.4.2", @@ -1605,8 +1629,8 @@ dependencies = [ "eth2_key_derivation", "eth2_ssz", "hex 0.4.2", - "hmac 0.8.1", - "pbkdf2 0.4.0", + "hmac 0.9.0", + "pbkdf2 0.5.0", "rand 0.7.3", "scrypt", "serde", @@ -1622,9 +1646,9 @@ dependencies = [ name = "eth2_libp2p" version = "0.2.0" dependencies = [ - "base64 0.12.3", + "base64 0.13.0", "directory", - "dirs", + "dirs 3.0.1", "discv5", "error-chain", "eth2_ssz", @@ -1843,15 +1867,15 @@ checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" [[package]] name = "flate2" -version = "1.0.18" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da80be589a72651dcda34d8b35bcdc9b7254ad06325611074d9cc0fbb19f60ee" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" dependencies = [ "cfg-if", "crc32fast", "libc", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.3.7", ] [[package]] @@ -1913,6 +1937,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "funty" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8" + [[package]] name = "futures" version = "0.1.29" @@ -2214,12 +2244,12 @@ checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" [[package]] name = "hashbrown" -version = "0.6.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ "ahash", - "autocfg 0.1.7", + "autocfg 1.0.1", ] [[package]] @@ -2319,11 +2349,11 @@ dependencies = [ [[package]] name = "hmac" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" dependencies = [ - "crypto-mac 0.8.0", + "crypto-mac 0.9.1", "digest 0.9.0", ] @@ -2769,7 +2799,7 @@ dependencies = [ "clap_utils", "deposit_contract", "directory", - "dirs", + "dirs 3.0.1", "environment", "eth2_keystore", "eth2_libp2p", @@ -3100,9 +3130,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e704a02bcaecd4a08b93a23f6be59d0bd79cd161e0963e9499165a0a35df7bd" +checksum = "e3a245984b1b06c291f46e27ebda9f369a94a1ab8461d0e845e23f9ced01f5db" dependencies = [ "cc", "pkg-config", @@ -3116,6 +3146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" dependencies = [ "cc", + "libc", "pkg-config", "vcpkg", ] @@ -3220,11 +3251,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c456c123957de3a220cd03786e0d86aa542a88b46029973b542f426da6ef34" +checksum = "111b945ac72ec09eb7bc62a0fbdc3cc6e80555a7245f52a69d3921a75b53b153" dependencies = [ - "hashbrown 0.6.3", + "hashbrown 0.8.2", ] [[package]] @@ -3340,6 +3371,15 @@ dependencies = [ "unicase 2.6.0", ] +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + [[package]] name = "miniz_oxide" version = "0.4.2" @@ -3802,7 +3842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c740e5fbcb6847058b40ac7e5574766c6388f585e184d769910fe0d3a2ca861" dependencies = [ "arrayvec", - "bitvec", + "bitvec 0.17.4", "byte-slice-cast", "serde", ] @@ -3895,11 +3935,11 @@ dependencies = [ [[package]] name = "pbkdf2" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +checksum = "7170d73bf11f39b4ce1809aabc95bf5c33564cdc16fc3200ddda17a5f6e5e48b" dependencies = [ - "crypto-mac 0.8.0", + "crypto-mac 0.9.1", ] [[package]] @@ -3980,12 +4020,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "podio" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18befed8bc2b61abc79a457295e7e838417326da1586050b919414073977f19" - [[package]] name = "poly1305" version = "0.6.1" @@ -4059,15 +4093,16 @@ dependencies = [ [[package]] name = "prometheus" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0ced56dee39a6e960c15c74dc48849d614586db2eaada6497477af7c7811cd" +checksum = "30d70cf4412832bcac9cffe27906f4a66e450d323525e977168c70d1b36120ae" dependencies = [ "cfg-if", "fnv", "lazy_static", + "parking_lot 0.11.0", "protobuf", - "spin", + "regex", "thiserror", ] @@ -4221,9 +4256,9 @@ dependencies = [ [[package]] name = "r2d2_sqlite" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed60ebe88b27ac28c0563bc0fbeaecd302ff53e3a01e5ddc2ec9f4e6c707d929" +checksum = "227ab35ff4cbb01fa76da8f062590fe677b93c8d9e8415eb5fa981f2c1dba9d8" dependencies = [ "r2d2", "rusqlite", @@ -4235,6 +4270,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" +[[package]] +name = "radium" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a333b5f6adeff5a89f2e95dc2ea1ecb5319abbb56212afea6a37f87435338a5" + [[package]] name = "rand" version = "0.4.6" @@ -4572,9 +4613,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "4.0.5" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f" +checksum = "d755237fc0f99d98641540e66abac8bc46a0652f19148ac9e21de2da06b326c9" dependencies = [ "libc", "winapi 0.3.9", @@ -4582,9 +4623,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d0fd62e1df63d254714e6cb40d0a0e82e7a1623e7a27f679d851af092ae58b" +checksum = "4c78c3275d9d6eb684d2db4b2388546b32fdae0586c20a82f3905d21ea78b9ef" dependencies = [ "bitflags 1.2.1", "fallible-iterator", @@ -4593,7 +4634,6 @@ dependencies = [ "lru-cache", "memchr", "smallvec 1.4.2", - "time 0.1.44", ] [[package]] @@ -4723,12 +4763,12 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scrypt" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e7e75e27e8cd47e4be027d4b9fdc0b696116f981c22de21ca7bad63a9cb33a" +checksum = "3437654bbbe34054a268b3859fe41f871215069b39f0aef78808d85c37100696" dependencies = [ - "hmac 0.8.1", - "pbkdf2 0.4.0", + "hmac 0.9.0", + "pbkdf2 0.5.0", "sha2 0.9.1", ] @@ -5331,16 +5371,6 @@ dependencies = [ "types", ] -[[package]] -name = "stream-cipher" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f8ed9974042b8c3672ff3030a69fcc03b74c47c3d1ecb7755e8a3626011e88" -dependencies = [ - "block-cipher 0.7.1", - "generic-array 0.14.4", -] - [[package]] name = "stream-cipher" version = "0.7.1" @@ -5416,6 +5446,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" + [[package]] name = "target_info" version = "0.1.0" @@ -5464,7 +5500,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" dependencies = [ - "dirs", + "dirs 2.0.2", "winapi 0.3.9", ] @@ -6325,7 +6361,7 @@ dependencies = [ "clap_utils", "deposit_contract", "directory", - "dirs", + "dirs 3.0.1", "environment", "eth2", "eth2_config", @@ -6791,6 +6827,12 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "x25519-dalek" version = "1.1.0" @@ -6849,13 +6891,14 @@ dependencies = [ [[package]] name = "zip" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58287c28d78507f5f91f2a4cf1e8310e2c76fd4c6932f93ac60fd1ceb402db7d" +checksum = "543adf038106b64cfca4711c82c917d785e3540e04f7996554488f988ec43124" dependencies = [ + "byteorder", "bzip2", "crc32fast", "flate2", - "podio", + "thiserror", "time 0.1.44", ] diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index aaa5d52cc..b935948e9 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -6,20 +6,20 @@ edition = "2018" [dependencies] bls = { path = "../crypto/bls" } -clap = "2.33.0" +clap = "2.33.3" slog = "2.5.2" -slog-term = "2.5.0" +slog-term = "2.6.0" slog-async = "2.5.0" types = { path = "../consensus/types" } state_processing = { path = "../consensus/state_processing" } -dirs = "2.0.2" +dirs = "3.0.1" environment = { path = "../lighthouse/environment" } deposit_contract = { path = "../common/deposit_contract" } -libc = "0.2.65" +libc = "0.2.79" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" hex = "0.4.2" -rayon = "1.3.0" +rayon = "1.4.1" eth2_testnet_config = { path = "../common/eth2_testnet_config" } web3 = "0.11.0" futures = { version = "0.3.5", features = ["compat"] } @@ -27,7 +27,7 @@ clap_utils = { path = "../common/clap_utils" } directory = { path = "../common/directory" } eth2_wallet = { path = "../crypto/eth2_wallet" } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } -rand = "0.7.2" +rand = "0.7.3" validator_dir = { path = "../common/validator_dir" } tokio = { version = "0.2.22", features = ["full"] } eth2_keystore = { path = "../crypto/eth2_keystore" } diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index e1a8f4a14..57a3fd977 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -20,15 +20,15 @@ beacon_chain = { path = "beacon_chain" } types = { path = "../consensus/types" } store = { path = "./store" } client = { path = "client" } -clap = "2.33.0" +clap = "2.33.3" rand = "0.7.3" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } -slog-term = "2.5.0" +slog-term = "2.6.0" slog-async = "2.5.0" -ctrlc = { version = "3.1.4", features = ["termination"] } +ctrlc = { version = "3.1.6", features = ["termination"] } tokio = { version = "0.2.22", features = ["time"] } exit-future = "0.2.0" -dirs = "2.0.2" +dirs = "3.0.1" logging = { path = "../common/logging" } directory = {path = "../common/directory"} futures = "0.3.5" @@ -38,8 +38,8 @@ genesis = { path = "genesis" } eth2_testnet_config = { path = "../common/eth2_testnet_config" } eth2_libp2p = { path = "./eth2_libp2p" } eth2_ssz = "0.1.2" -serde = "1.0.110" +serde = "1.0.116" clap_utils = { path = "../common/clap_utils" } -hyper = "0.13.5" +hyper = "0.13.8" lighthouse_version = { path = "../common/lighthouse_version" } hex = "0.4.2" diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 9ffff370e..b249c1728 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -20,38 +20,38 @@ merkle_proof = { path = "../../consensus/merkle_proof" } store = { path = "../store" } parking_lot = "0.11.0" lazy_static = "1.4.0" -smallvec = "1.4.1" +smallvec = "1.4.2" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -log = "0.4.8" +log = "0.4.11" operation_pool = { path = "../operation_pool" } -rayon = "1.3.0" -serde = "1.0.110" -serde_derive = "1.0.110" -serde_yaml = "0.8.11" -serde_json = "1.0.52" +rayon = "1.4.1" +serde = "1.0.116" +serde_derive = "1.0.116" +serde_yaml = "0.8.13" +serde_json = "1.0.58" slog = { version = "2.5.2", features = ["max_level_trace"] } slog-term = "2.6.0" -sloggers = "1.0.0" +sloggers = "1.0.1" slot_clock = { path = "../../common/slot_clock" } eth2_hashing = "0.1.0" eth2_ssz = "0.1.2" eth2_ssz_types = { path = "../../consensus/ssz_types" } eth2_ssz_derive = "0.1.0" state_processing = { path = "../../consensus/state_processing" } -tree_hash = "0.1.0" +tree_hash = "0.1.1" types = { path = "../../consensus/types" } tokio = "0.2.22" eth1 = { path = "../eth1" } websocket_server = { path = "../websocket_server" } futures = "0.3.5" genesis = { path = "../genesis" } -integer-sqrt = "0.1.3" +integer-sqrt = "0.1.5" rand = "0.7.3" rand_core = "0.5.1" proto_array = { path = "../../consensus/proto_array" } -lru = "0.5.1" +lru = "0.6.0" tempfile = "3.1.0" -bitvec = "0.17.4" +bitvec = "0.19.3" bls = { path = "../../crypto/bls" } safe_arith = { path = "../../consensus/safe_arith" } fork_choice = { path = "../../consensus/fork_choice" } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 16a773396..8114761aa 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"] edition = "2018" [dev-dependencies] -sloggers = "1.0.0" +sloggers = "1.0.1" toml = "0.5.6" [dependencies] @@ -16,21 +16,21 @@ timer = { path = "../timer" } eth2_libp2p = { path = "../eth2_libp2p" } parking_lot = "0.11.0" websocket_server = { path = "../websocket_server" } -prometheus = "0.9.0" +prometheus = "0.10.0" types = { path = "../../consensus/types" } -tree_hash = "0.1.0" +tree_hash = "0.1.1" eth2_config = { path = "../../common/eth2_config" } slot_clock = { path = "../../common/slot_clock" } -serde = "1.0.110" -serde_derive = "1.0.110" -error-chain = "0.12.2" -serde_yaml = "0.8.11" +serde = "1.0.116" +serde_derive = "1.0.116" +error-chain = "0.12.4" +serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace"] } slog-async = "2.5.0" tokio = "0.2.22" -dirs = "2.0.2" +dirs = "3.0.1" futures = "0.3.5" -reqwest = { version = "0.10.4", features = ["native-tls-vendored"] } +reqwest = { version = "0.10.8", features = ["native-tls-vendored"] } url = "2.1.1" eth1 = { path = "../eth1" } genesis = { path = "../genesis" } @@ -39,7 +39,7 @@ environment = { path = "../../lighthouse/environment" } eth2_ssz = "0.1.2" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -time = "0.2.16" +time = "0.2.22" bus = "2.2.3" directory = {path = "../../common/directory"} http_api = { path = "../http_api" } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 217444ff6..602c3e90c 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -8,26 +8,26 @@ edition = "2018" eth1_test_rig = { path = "../../testing/eth1_test_rig" } toml = "0.5.6" web3 = "0.11.0" -sloggers = "1.0.0" +sloggers = "1.0.1" environment = { path = "../../lighthouse/environment" } [dependencies] -reqwest = { version = "0.10.4", features = ["native-tls-vendored"] } +reqwest = { version = "0.10.8", features = ["native-tls-vendored"] } futures = { version = "0.3.5", features = ["compat"] } -serde_json = "1.0.52" -serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.58" +serde = { version = "1.0.116", features = ["derive"] } hex = "0.4.2" types = { path = "../../consensus/types"} merkle_proof = { path = "../../consensus/merkle_proof"} eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -tree_hash = "0.1.0" +tree_hash = "0.1.1" eth2_hashing = "0.1.0" parking_lot = "0.11.0" slog = "2.5.2" tokio = { version = "0.2.22", features = ["full"] } state_processing = { path = "../../consensus/state_processing" } -libflate = "1.0.0" +libflate = "1.0.2" lighthouse_metrics = { path = "../../common/lighthouse_metrics"} lazy_static = "1.4.0" task_executor = { path = "../../common/task_executor" } diff --git a/beacon_node/eth2_libp2p/Cargo.toml b/beacon_node/eth2_libp2p/Cargo.toml index bb2c88847..82cf8cf79 100644 --- a/beacon_node/eth2_libp2p/Cargo.toml +++ b/beacon_node/eth2_libp2p/Cargo.toml @@ -9,26 +9,26 @@ discv5 = { version = "0.1.0-alpha.13", features = ["libp2p"] } types = { path = "../../consensus/types" } hashset_delay = { path = "../../common/hashset_delay" } eth2_ssz_types = { path = "../../consensus/ssz_types" } -serde = { version = "1.0.110", features = ["derive"] } -serde_derive = "1.0.110" +serde = { version = "1.0.116", features = ["derive"] } +serde_derive = "1.0.116" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" slog = { version = "2.5.2", features = ["max_level_trace"] } lighthouse_version = { path = "../../common/lighthouse_version" } tokio = { version = "0.2.22", features = ["time", "macros"] } futures = "0.3.5" -error-chain = "0.12.2" -dirs = "2.0.2" +error-chain = "0.12.4" +dirs = "3.0.1" fnv = "1.0.7" unsigned-varint = { git = "https://github.com/sigp/unsigned-varint", branch = "latest-codecs", features = ["codec"] } lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -smallvec = "1.4.1" -lru = "0.5.1" +smallvec = "1.4.2" +lru = "0.6.0" parking_lot = "0.11.0" sha2 = "0.9.1" -base64 = "0.12.1" -snap = "1.0.0" +base64 = "0.13.0" +snap = "1.0.1" void = "1.0.2" hex = "0.4.2" tokio-io-timeout = "0.4.0" @@ -48,7 +48,7 @@ features = ["websocket", "identify", "mplex", "noise", "gossipsub", "dns", "tcp- [dev-dependencies] tokio = { version = "0.2.22", features = ["full"] } -slog-term = "2.5.0" +slog-term = "2.6.0" slog-async = "2.5.0" tempdir = "0.3.7" exit-future = "0.2.0" diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index 9510a840d..2150754ea 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -12,16 +12,16 @@ futures = "0.3.5" types = { path = "../../consensus/types"} environment = { path = "../../lighthouse/environment"} eth1 = { path = "../eth1"} -rayon = "1.3.0" +rayon = "1.4.1" state_processing = { path = "../../consensus/state_processing" } merkle_proof = { path = "../../consensus/merkle_proof" } eth2_ssz = "0.1.2" eth2_hashing = "0.1.0" -tree_hash = "0.1.0" +tree_hash = "0.1.1" tokio = { version = "0.2.22", features = ["full"] } parking_lot = "0.11.0" slog = "2.5.2" exit-future = "0.2.0" -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" int_to_bytes = { path = "../../consensus/int_to_bytes" } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 0fd2ca5cb..af2ebfeca 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] warp = "0.2.5" -serde = { version = "1.0.110", features = ["derive"] } +serde = { version = "1.0.116", features = ["derive"] } tokio = { version = "0.2.22", features = ["macros"] } parking_lot = "0.11.0" types = { path = "../../consensus/types" } @@ -28,5 +28,5 @@ slot_clock = { path = "../../common/slot_clock" } [dev-dependencies] store = { path = "../store" } environment = { path = "../../lighthouse/environment" } -tree_hash = { path = "../../consensus/tree_hash" } -discv5 = { version = "0.1.0-alpha.10", features = ["libp2p"] } +tree_hash = "0.1.1" +discv5 = { version = "0.1.0-alpha.13", features = ["libp2p"] } diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index 482f7a5de..1b9197917 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -7,9 +7,9 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prometheus = "0.9.0" +prometheus = "0.10.0" warp = "0.2.5" -serde = { version = "1.0.110", features = ["derive"] } +serde = { version = "1.0.116", features = ["derive"] } slog = "2.5.2" beacon_chain = { path = "../beacon_chain" } store = { path = "../store" } @@ -22,7 +22,7 @@ lighthouse_version = { path = "../../common/lighthouse_version" } warp_utils = { path = "../../common/warp_utils" } [dev-dependencies] -tokio = { version = "0.2.21", features = ["sync"] } +tokio = { version = "0.2.22", features = ["sync"] } reqwest = { version = "0.10.8", features = ["json"] } environment = { path = "../../lighthouse/environment" } types = { path = "../../consensus/types" } diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 2bb2a94ea..c2d81bf9d 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"] edition = "2018" [dev-dependencies] -sloggers = "1.0.0" +sloggers = "1.0.1" genesis = { path = "../genesis" } lazy_static = "1.4.0" matches = "0.1.8" @@ -24,15 +24,15 @@ slog = { version = "2.5.2", features = ["max_level_trace"] } hex = "0.4.2" eth2_ssz = "0.1.2" eth2_ssz_types = { path = "../../consensus/ssz_types" } -tree_hash = "0.1.0" +tree_hash = "0.1.1" futures = "0.3.5" -error-chain = "0.12.2" +error-chain = "0.12.4" tokio = { version = "0.2.22", features = ["full"] } parking_lot = "0.11.0" -smallvec = "1.4.1" +smallvec = "1.4.2" rand = "0.7.3" -fnv = "1.0.6" -rlp = "0.4.5" +fnv = "1.0.7" +rlp = "0.4.6" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } task_executor = { path = "../../common/task_executor" } diff --git a/beacon_node/operation_pool/Cargo.toml b/beacon_node/operation_pool/Cargo.toml index d7309d412..c16ab8fb5 100644 --- a/beacon_node/operation_pool/Cargo.toml +++ b/beacon_node/operation_pool/Cargo.toml @@ -11,8 +11,8 @@ types = { path = "../../consensus/types" } state_processing = { path = "../../consensus/state_processing" } eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" store = { path = "../store" } [dev-dependencies] diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 9f06bca54..a0fa4c24e 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -10,8 +10,8 @@ harness = false [dev-dependencies] tempfile = "3.1.0" -criterion = "0.3.2" -rayon = "1.3.0" +criterion = "0.3.3" +rayon = "1.4.1" [dependencies] db-key = "0.0.5" @@ -20,13 +20,13 @@ parking_lot = "0.11.0" itertools = "0.9.0" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -tree_hash = "0.1.0" +tree_hash = "0.1.1" types = { path = "../../consensus/types" } state_processing = { path = "../../consensus/state_processing" } slog = "2.5.2" -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -lru = "0.5.1" -sloggers = "1.0.0" +lru = "0.6.0" +sloggers = "1.0.1" diff --git a/beacon_node/websocket_server/Cargo.toml b/beacon_node/websocket_server/Cargo.toml index 902eb588b..8a10bdc5b 100644 --- a/beacon_node/websocket_server/Cargo.toml +++ b/beacon_node/websocket_server/Cargo.toml @@ -8,8 +8,8 @@ edition = "2018" [dependencies] futures = "0.3.5" -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" slog = "2.5.2" tokio = { version = "0.2.22", features = ["full"] } types = { path = "../../consensus/types" } diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index a5895f4c3..2c0ebdc68 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -6,15 +6,15 @@ edition = "2018" [dependencies] beacon_node = { path = "../beacon_node" } -clap = "2.33.0" +clap = "2.33.3" eth2_libp2p = { path = "../beacon_node/eth2_libp2p" } types = { path = "../consensus/types" } eth2_testnet_config = { path = "../common/eth2_testnet_config" } -eth2_ssz = { path = "../consensus/ssz" } +eth2_ssz = "0.1.2" slog = "2.5.2" sloggers = "1.0.1" tokio = "0.2.22" -log = "0.4.8" +log = "0.4.11" slog-term = "2.6.0" logging = { path = "../common/logging" } slog-async = "2.5.0" diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index feb8f6467..7b8b2822e 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = "0.7.2" +rand = "0.7.3" eth2_wallet = { path = "../../crypto/eth2_wallet" } eth2_keystore = { path = "../../crypto/eth2_keystore" } -zeroize = { version = "1.0.0", features = ["zeroize_derive"] } -serde = "1.0.110" -serde_derive = "1.0.110" +zeroize = { version = "1.1.1", features = ["zeroize_derive"] } +serde = "1.0.116" +serde_derive = "1.0.116" serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } types = { path = "../../consensus/types" } validator_dir = { path = "../validator_dir" } regex = "1.3.9" -rpassword = "4.0.5" +rpassword = "5.0.0" diff --git a/common/clap_utils/Cargo.toml b/common/clap_utils/Cargo.toml index 0f84869ad..85c562a50 100644 --- a/common/clap_utils/Cargo.toml +++ b/common/clap_utils/Cargo.toml @@ -7,9 +7,9 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "2.33.0" +clap = "2.33.3" hex = "0.4.2" -dirs = "2.0.2" +dirs = "3.0.1" types = { path = "../../consensus/types" } eth2_testnet_config = { path = "../eth2_testnet_config" } eth2_ssz = "0.1.2" diff --git a/common/compare_fields_derive/Cargo.toml b/common/compare_fields_derive/Cargo.toml index 550615b14..256af2767 100644 --- a/common/compare_fields_derive/Cargo.toml +++ b/common/compare_fields_derive/Cargo.toml @@ -8,5 +8,5 @@ edition = "2018" proc-macro = true [dependencies] -syn = "1.0.18" -quote = "1.0.4" +syn = "1.0.42" +quote = "1.0.7" diff --git a/common/deposit_contract/Cargo.toml b/common/deposit_contract/Cargo.toml index 02014305d..1a6f84395 100644 --- a/common/deposit_contract/Cargo.toml +++ b/common/deposit_contract/Cargo.toml @@ -7,13 +7,13 @@ edition = "2018" build = "build.rs" [build-dependencies] -reqwest = { version = "0.10.4", features = ["blocking", "json", "native-tls-vendored"] } -serde_json = "1.0.52" +reqwest = { version = "0.10.8", features = ["blocking", "json", "native-tls-vendored"] } +serde_json = "1.0.58" sha2 = "0.9.1" hex = "0.4.2" [dependencies] types = { path = "../../consensus/types"} eth2_ssz = "0.1.2" -tree_hash = "0.1.0" +tree_hash = "0.1.1" ethabi = "12.0.0" diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml index ebea5f3dc..1687bb48b 100644 --- a/common/directory/Cargo.toml +++ b/common/directory/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "2.33.0" +clap = "2.33.3" clap_utils = {path = "../clap_utils"} -dirs = "2.0.2" +dirs = "3.0.1" eth2_testnet_config = { path = "../eth2_testnet_config" } diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 479630c9c..0c1528e9b 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { version = "1.0.110", features = ["derive"] } -serde_json = "1.0.52" +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.58" types = { path = "../../consensus/types" } hex = "0.4.2" reqwest = { version = "0.10.8", features = ["json"] } eth2_libp2p = { path = "../../beacon_node/eth2_libp2p" } proto_array = { path = "../../consensus/proto_array", optional = true } serde_utils = { path = "../../consensus/serde_utils" } -zeroize = { version = "1.0.0", features = ["zeroize_derive"] } +zeroize = { version = "1.1.1", features = ["zeroize_derive"] } eth2_keystore = { path = "../../crypto/eth2_keystore" } libsecp256k1 = "0.3.5" ring = "0.16.12" @@ -23,7 +23,7 @@ bytes = "0.5.6" account_utils = { path = "../../common/account_utils" } [target.'cfg(target_os = "linux")'.dependencies] -psutil = { version = "3.1.0", optional = true } +psutil = { version = "3.2.0", optional = true } procinfo = { version = "0.4.2", optional = true } [features] diff --git a/common/eth2_config/Cargo.toml b/common/eth2_config/Cargo.toml index b551a2349..cb5ee888d 100644 --- a/common/eth2_config/Cargo.toml +++ b/common/eth2_config/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" [dependencies] -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" toml = "0.5.6" types = { path = "../../consensus/types" } diff --git a/common/eth2_interop_keypairs/Cargo.toml b/common/eth2_interop_keypairs/Cargo.toml index 6062257cd..c451e068c 100644 --- a/common/eth2_interop_keypairs/Cargo.toml +++ b/common/eth2_interop_keypairs/Cargo.toml @@ -11,10 +11,10 @@ lazy_static = "1.4.0" num-bigint = "0.3.0" eth2_hashing = "0.1.0" hex = "0.4.2" -serde_yaml = "0.8.11" -serde = "1.0.110" -serde_derive = "1.0.110" +serde_yaml = "0.8.13" +serde = "1.0.116" +serde_derive = "1.0.116" bls = { path = "../../crypto/bls" } [dev-dependencies] -base64 = "0.12.1" +base64 = "0.13.0" diff --git a/common/eth2_testnet_config/Cargo.toml b/common/eth2_testnet_config/Cargo.toml index dceb5bec7..7ee8eb801 100644 --- a/common/eth2_testnet_config/Cargo.toml +++ b/common/eth2_testnet_config/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" build = "build.rs" [build-dependencies] -zip = "0.5" +zip = "0.5.8" eth2_config = { path = "../eth2_config"} [dev-dependencies] tempdir = "0.3.7" [dependencies] -serde = "1.0.110" -serde_yaml = "0.8.11" +serde = "1.0.116" +serde_yaml = "0.8.13" types = { path = "../../consensus/types"} eth2_ssz = "0.1.2" eth2_config = { path = "../eth2_config"} diff --git a/common/lighthouse_metrics/Cargo.toml b/common/lighthouse_metrics/Cargo.toml index c6f11391f..a8380b85c 100644 --- a/common/lighthouse_metrics/Cargo.toml +++ b/common/lighthouse_metrics/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" [dependencies] lazy_static = "1.4.0" -prometheus = "0.9.0" +prometheus = "0.10.0" diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index b3c50c6d6..8e19c1f11 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -6,6 +6,6 @@ edition = "2018" [dependencies] slog = "2.5.2" -slog-term = "2.5.0" +slog-term = "2.6.0" lighthouse_metrics = { path = "../lighthouse_metrics" } lazy_static = "1.4.0" diff --git a/common/test_random_derive/Cargo.toml b/common/test_random_derive/Cargo.toml index a02cb7fda..0186ab326 100644 --- a/common/test_random_derive/Cargo.toml +++ b/common/test_random_derive/Cargo.toml @@ -9,5 +9,5 @@ description = "Procedural derive macros for implementation of TestRandom trait" proc-macro = true [dependencies] -syn = "1.0.18" -quote = "1.0.4" +syn = "1.0.42" +quote = "1.0.7" diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 9a833d2f5..df8eed3d0 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -13,10 +13,10 @@ insecure_keys = [] bls = { path = "../../crypto/bls" } eth2_keystore = { path = "../../crypto/eth2_keystore" } types = { path = "../../consensus/types" } -rand = "0.7.2" +rand = "0.7.3" deposit_contract = { path = "../deposit_contract" } -rayon = "1.3.0" -tree_hash = { path = "../../consensus/tree_hash" } +rayon = "1.4.1" +tree_hash = "0.1.1" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } hex = "0.4.2" diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index 5d4a0fbbc..dd029e3c2 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -13,5 +13,5 @@ types = { path = "../../consensus/types" } beacon_chain = { path = "../../beacon_node/beacon_chain" } state_processing = { path = "../../consensus/state_processing" } safe_arith = { path = "../../consensus/safe_arith" } -serde = { version = "1.0.110", features = ["derive"] } -tokio = { version = "0.2.21", features = ["sync"] } +serde = { version = "1.0.116", features = ["derive"] } +tokio = { version = "0.2.22", features = ["sync"] } diff --git a/consensus/cached_tree_hash/Cargo.toml b/consensus/cached_tree_hash/Cargo.toml index 8cb796edd..5de54fe69 100644 --- a/consensus/cached_tree_hash/Cargo.toml +++ b/consensus/cached_tree_hash/Cargo.toml @@ -5,13 +5,13 @@ authors = ["Michael Sproul <michael@sigmaprime.io>"] edition = "2018" [dependencies] -ethereum-types = "0.9.1" +ethereum-types = "0.9.2" eth2_ssz_types = { path = "../ssz_types" } eth2_hashing = "0.1.0" eth2_ssz_derive = "0.1.0" eth2_ssz = "0.1.2" -tree_hash = "0.1.0" -smallvec = "1.4.1" +tree_hash = "0.1.1" +smallvec = "1.4.2" [dev-dependencies] quickcheck = "0.9.2" diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index e07111407..a5724bc1a 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -9,13 +9,13 @@ edition = "2018" [dependencies] types = { path = "../types" } proto_array = { path = "../proto_array" } -eth2_ssz = { path = "../ssz" } -eth2_ssz_derive = { path = "../ssz_derive" } +eth2_ssz = "0.1.2" +eth2_ssz_derive = "0.1.0" [dev-dependencies] state_processing = { path = "../../consensus/state_processing" } beacon_chain = { path = "../../beacon_node/beacon_chain" } store = { path = "../../beacon_node/store" } -tree_hash = { path = "../../consensus/tree_hash" } +tree_hash = "0.1.1" slot_clock = { path = "../../common/slot_clock" } hex = "0.4.2" diff --git a/consensus/int_to_bytes/Cargo.toml b/consensus/int_to_bytes/Cargo.toml index 87839ccaa..736fed9c8 100644 --- a/consensus/int_to_bytes/Cargo.toml +++ b/consensus/int_to_bytes/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" [dependencies] -bytes = "0.5.4" +bytes = "0.5.6" [dev-dependencies] -yaml-rust = "0.4.3" +yaml-rust = "0.4.4" hex = "0.4.2" diff --git a/consensus/merkle_proof/Cargo.toml b/consensus/merkle_proof/Cargo.toml index 84745d224..7c0ff5207 100644 --- a/consensus/merkle_proof/Cargo.toml +++ b/consensus/merkle_proof/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Michael Sproul <michael@sigmaprime.io>"] edition = "2018" [dependencies] -ethereum-types = "0.9.1" +ethereum-types = "0.9.2" eth2_hashing = "0.1.0" lazy_static = "1.4.0" safe_arith = { path = "../safe_arith" } diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index 63f2d7fd7..111f24a3b 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -12,6 +12,6 @@ path = "src/bin.rs" types = { path = "../types" } eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -serde = "1.0.110" -serde_derive = "1.0.110" -serde_yaml = "0.8.11" +serde = "1.0.116" +serde_derive = "1.0.116" +serde_yaml = "0.8.13" diff --git a/consensus/serde_utils/Cargo.toml b/consensus/serde_utils/Cargo.toml index 8c0013562..060179846 100644 --- a/consensus/serde_utils/Cargo.toml +++ b/consensus/serde_utils/Cargo.toml @@ -5,9 +5,9 @@ authors = ["Paul Hauner <paul@paulhauner.com", "Michael Sproul <michael@sigmapri edition = "2018" [dependencies] -serde = { version = "1.0.110", features = ["derive"] } -serde_derive = "1.0.110" +serde = { version = "1.0.116", features = ["derive"] } +serde_derive = "1.0.116" hex = "0.4.2" [dev-dependencies] -serde_json = "1.0.52" +serde_json = "1.0.58" diff --git a/consensus/ssz/Cargo.toml b/consensus/ssz/Cargo.toml index c71593301..63f98588a 100644 --- a/consensus/ssz/Cargo.toml +++ b/consensus/ssz/Cargo.toml @@ -13,8 +13,8 @@ name = "ssz" eth2_ssz_derive = "0.1.0" [dependencies] -ethereum-types = "0.9.1" -smallvec = "1.4.1" +ethereum-types = "0.9.2" +smallvec = "1.4.2" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/consensus/ssz_derive/Cargo.toml b/consensus/ssz_derive/Cargo.toml index e074f001a..fb0de111c 100644 --- a/consensus/ssz_derive/Cargo.toml +++ b/consensus/ssz_derive/Cargo.toml @@ -11,5 +11,5 @@ name = "ssz_derive" proc-macro = true [dependencies] -syn = "1.0.18" -quote = "1.0.4" +syn = "1.0.42" +quote = "1.0.7" diff --git a/consensus/ssz_types/Cargo.toml b/consensus/ssz_types/Cargo.toml index ca6a5adbe..a0ea0dbcd 100644 --- a/consensus/ssz_types/Cargo.toml +++ b/consensus/ssz_types/Cargo.toml @@ -8,13 +8,13 @@ edition = "2018" name = "ssz_types" [dependencies] -tree_hash = "0.1.0" -serde = "1.0.110" -serde_derive = "1.0.110" +tree_hash = "0.1.1" +serde = "1.0.116" +serde_derive = "1.0.116" serde_utils = { path = "../serde_utils" } eth2_ssz = "0.1.2" typenum = "1.12.0" -arbitrary = { version = "0.4.4", features = ["derive"], optional = true } +arbitrary = { version = "0.4.6", features = ["derive"], optional = true } [dev-dependencies] tree_hash_derive = "0.2.0" diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index bd0de6c19..98ce4ff55 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -9,29 +9,29 @@ name = "benches" harness = false [dev-dependencies] -criterion = "0.3.2" +criterion = "0.3.3" env_logger = "0.7.1" -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" lazy_static = "1.4.0" -serde_yaml = "0.8.11" +serde_yaml = "0.8.13" [dependencies] bls = { path = "../../crypto/bls" } -integer-sqrt = "0.1.3" +integer-sqrt = "0.1.5" itertools = "0.9.0" eth2_ssz = "0.1.2" eth2_ssz_types = { path = "../ssz_types" } merkle_proof = { path = "../merkle_proof" } -log = "0.4.8" +log = "0.4.11" safe_arith = { path = "../safe_arith" } -tree_hash = "0.1.0" +tree_hash = "0.1.1" tree_hash_derive = "0.2.0" types = { path = "../types", default-features = false } -rayon = "1.3.0" +rayon = "1.4.1" eth2_hashing = "0.1.0" int_to_bytes = { path = "../int_to_bytes" } -arbitrary = { version = "0.4.4", features = ["derive"], optional = true } +arbitrary = { version = "0.4.6", features = ["derive"], optional = true } [features] default = ["legacy-arith"] diff --git a/consensus/swap_or_not_shuffle/Cargo.toml b/consensus/swap_or_not_shuffle/Cargo.toml index 12af74aff..118e471b0 100644 --- a/consensus/swap_or_not_shuffle/Cargo.toml +++ b/consensus/swap_or_not_shuffle/Cargo.toml @@ -9,11 +9,11 @@ name = "benches" harness = false [dev-dependencies] -criterion = "0.3.2" +criterion = "0.3.3" [dependencies] eth2_hashing = "0.1.0" -ethereum-types = "0.9.1" +ethereum-types = "0.9.2" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/consensus/tree_hash/Cargo.toml b/consensus/tree_hash/Cargo.toml index 436805558..5837afd85 100644 --- a/consensus/tree_hash/Cargo.toml +++ b/consensus/tree_hash/Cargo.toml @@ -11,16 +11,16 @@ name = "benches" harness = false [dev-dependencies] -criterion = "0.3.2" +criterion = "0.3.3" rand = "0.7.3" tree_hash_derive = "0.2.0" types = { path = "../types" } lazy_static = "1.4.0" [dependencies] -ethereum-types = "0.9.1" +ethereum-types = "0.9.2" eth2_hashing = "0.1.0" -smallvec = "1.4.1" +smallvec = "1.4.2" [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/consensus/tree_hash_derive/Cargo.toml b/consensus/tree_hash_derive/Cargo.toml index 11caabe07..10c743cb3 100644 --- a/consensus/tree_hash_derive/Cargo.toml +++ b/consensus/tree_hash_derive/Cargo.toml @@ -10,5 +10,5 @@ license = "Apache-2.0" proc-macro = true [dependencies] -syn = "1.0.18" -quote = "1.0.4" +syn = "1.0.42" +quote = "1.0.7" diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index c3a5cd90d..8984beeda 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -13,38 +13,38 @@ bls = { path = "../../crypto/bls" } compare_fields = { path = "../../common/compare_fields" } compare_fields_derive = { path = "../../common/compare_fields_derive" } eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } -ethereum-types = "0.9.1" +ethereum-types = "0.9.2" eth2_hashing = "0.1.0" hex = "0.4.2" int_to_bytes = { path = "../int_to_bytes" } -log = "0.4.8" +log = "0.4.11" merkle_proof = { path = "../merkle_proof" } -rayon = "1.3.0" +rayon = "1.4.1" rand = "0.7.3" safe_arith = { path = "../safe_arith" } -serde = "1.0.110" -serde_derive = "1.0.110" +serde = "1.0.116" +serde_derive = "1.0.116" slog = "2.5.2" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" eth2_ssz_types = { path = "../ssz_types" } swap_or_not_shuffle = { path = "../swap_or_not_shuffle" } test_random_derive = { path = "../../common/test_random_derive" } -tree_hash = "0.1.0" +tree_hash = "0.1.1" tree_hash_derive = "0.2.0" rand_xorshift = "0.2.0" cached_tree_hash = { path = "../cached_tree_hash" } -serde_yaml = "0.8.11" +serde_yaml = "0.8.13" tempfile = "3.1.0" derivative = "2.1.1" -rusqlite = { version = "0.23.1", features = ["bundled"], optional = true } -arbitrary = { version = "0.4.4", features = ["derive"], optional = true } +rusqlite = { version = "0.24.0", features = ["bundled"], optional = true } +arbitrary = { version = "0.4.6", features = ["derive"], optional = true } serde_utils = { path = "../serde_utils" } regex = "1.3.9" [dev-dependencies] -serde_json = "1.0.52" -criterion = "0.3.2" +serde_json = "1.0.58" +criterion = "0.3.3" [features] default = ["sqlite", "legacy-arith"] diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index 8fd004a80..7461a70b6 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -6,17 +6,17 @@ edition = "2018" [dependencies] eth2_ssz = "0.1.2" -tree_hash = "0.1.0" +tree_hash = "0.1.1" milagro_bls = { git = "https://github.com/sigp/milagro_bls", branch = "paulh" } -rand = "0.7.2" -serde = "1.0.102" -serde_derive = "1.0.102" +rand = "0.7.3" +serde = "1.0.116" +serde_derive = "1.0.116" serde_utils = { path = "../../consensus/serde_utils" } -hex = "0.3" +hex = "0.4.2" eth2_hashing = "0.1.0" -ethereum-types = "0.9.1" -arbitrary = { version = "0.4.4", features = ["derive"], optional = true } -zeroize = { version = "1.0.0", features = ["zeroize_derive"] } +ethereum-types = "0.9.2" +arbitrary = { version = "0.4.6", features = ["derive"], optional = true } +zeroize = { version = "1.1.1", features = ["zeroize_derive"] } blst = { git = "https://github.com/sigp/blst.git", rev = "284f7059642851c760a09fb1708bcb59c7ca323c" } [features] diff --git a/crypto/eth2_hashing/Cargo.toml b/crypto/eth2_hashing/Cargo.toml index 6f265b64d..21d1a9e9f 100644 --- a/crypto/eth2_hashing/Cargo.toml +++ b/crypto/eth2_hashing/Cargo.toml @@ -19,7 +19,7 @@ sha2 = "0.9.1" rustc-hex = "2.1.0" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.12" +wasm-bindgen-test = "0.3.18" [features] default = ["zero_hash_cache"] diff --git a/crypto/eth2_key_derivation/Cargo.toml b/crypto/eth2_key_derivation/Cargo.toml index 610f7a566..fea5eb67e 100644 --- a/crypto/eth2_key_derivation/Cargo.toml +++ b/crypto/eth2_key_derivation/Cargo.toml @@ -7,10 +7,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sha2 = "0.9.0" -zeroize = { version = "1.0.0", features = ["zeroize_derive"] } +sha2 = "0.9.1" +zeroize = { version = "1.1.1", features = ["zeroize_derive"] } num-bigint-dig = { version = "0.6.0", features = ["zeroize"] } -ring = "0.16.9" +ring = "0.16.12" bls = { path = "../bls" } [dev-dependencies] diff --git a/crypto/eth2_keystore/Cargo.toml b/crypto/eth2_keystore/Cargo.toml index 3a3eb4a80..63508b5e5 100644 --- a/crypto/eth2_keystore/Cargo.toml +++ b/crypto/eth2_keystore/Cargo.toml @@ -7,20 +7,20 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = "0.7.2" -aes-ctr = "0.4.0" -hmac = "0.8.0" -pbkdf2 = { version = "0.4.0", default-features = false } -scrypt = { version = "0.3.0", default-features = false } -sha2 = "0.9.0" -uuid = { version = "0.8", features = ["serde", "v4"] } -zeroize = { version = "1.0.0", features = ["zeroize_derive"] } -serde = "1.0.110" -serde_repr = "0.1" +rand = "0.7.3" +aes-ctr = "0.5.0" +hmac = "0.9.0" +pbkdf2 = { version = "0.5.0", default-features = false } +scrypt = { version = "0.4.1", default-features = false } +sha2 = "0.9.1" +uuid = { version = "0.8.1", features = ["serde", "v4"] } +zeroize = { version = "1.1.1", features = ["zeroize_derive"] } +serde = "1.0.116" +serde_repr = "0.1.6" hex = "0.4.2" bls = { path = "../bls" } -eth2_ssz = { path = "../../consensus/ssz" } -serde_json = "1.0.41" +eth2_ssz = "0.1.2" +serde_json = "1.0.58" eth2_key_derivation = { path = "../eth2_key_derivation" } [dev-dependencies] tempfile = "3.1.0" diff --git a/crypto/eth2_wallet/Cargo.toml b/crypto/eth2_wallet/Cargo.toml index 47e6e02d9..e34ee1730 100644 --- a/crypto/eth2_wallet/Cargo.toml +++ b/crypto/eth2_wallet/Cargo.toml @@ -7,11 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = "1.0.110" -serde_json = "1.0.41" -serde_repr = "0.1" -uuid = { version = "0.8", features = ["serde", "v4"] } -rand = "0.7.2" +serde = "1.0.116" +serde_json = "1.0.58" +serde_repr = "0.1.6" +uuid = { version = "0.8.1", features = ["serde", "v4"] } +rand = "0.7.3" eth2_keystore = { path = "../eth2_keystore" } eth2_key_derivation = { path = "../eth2_key_derivation" } tiny-bip39 = { git = "https://github.com/sigp/tiny-bip39.git", rev = "1137c32da91bd5e75db4305a84ddd15255423f7f" } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 5df1b4d16..ea34a149c 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -10,29 +10,29 @@ portable = ["bls/supranational-portable"] [dependencies] bls = { path = "../crypto/bls" } -clap = "2.33.0" +clap = "2.33.3" hex = "0.4.2" -log = "0.4.8" -serde = "1.0.110" -serde_yaml = "0.8.11" -simple_logger = "1.6.0" +log = "0.4.11" +serde = "1.0.116" +serde_yaml = "0.8.13" +simple_logger = "1.10.0" types = { path = "../consensus/types" } state_processing = { path = "../consensus/state_processing" } eth2_ssz = "0.1.2" -regex = "1.3.7" +regex = "1.3.9" futures = { version = "0.3.5", features = ["compat"] } environment = { path = "../lighthouse/environment" } web3 = "0.11.0" eth2_testnet_config = { path = "../common/eth2_testnet_config" } -dirs = "2.0.2" +dirs = "3.0.1" genesis = { path = "../beacon_node/genesis" } deposit_contract = { path = "../common/deposit_contract" } -tree_hash = "0.1.0" +tree_hash = "0.1.1" tokio = { version = "0.2.22", features = ["full"] } clap_utils = { path = "../common/clap_utils" } eth2_libp2p = { path = "../beacon_node/eth2_libp2p" } validator_dir = { path = "../common/validator_dir", features = ["insecure_keys"] } -rand = "0.7.2" +rand = "0.7.3" eth2_keystore = { path = "../crypto/eth2_keystore" } lighthouse_version = { path = "../common/lighthouse_version" } directory = { path = "../common/directory" } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 103717506..ad1c359b7 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -16,13 +16,13 @@ milagro = ["bls/milagro"] beacon_node = { "path" = "../beacon_node" } tokio = "0.2.22" slog = { version = "2.5.2", features = ["max_level_trace"] } -sloggers = "1.0.0" +sloggers = "1.0.1" types = { "path" = "../consensus/types" } bls = { path = "../crypto/bls" } -clap = "2.33.0" +clap = "2.33.3" env_logger = "0.7.1" logging = { path = "../common/logging" } -slog-term = "2.5.0" +slog-term = "2.6.0" slog-async = "2.5.0" environment = { path = "./environment" } boot_node = { path = "../boot_node" } diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 9cb8eeaab..7bb3faa6a 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" [dependencies] tokio = { version = "0.2.22", features = ["macros"] } slog = { version = "2.5.2", features = ["max_level_trace"] } -sloggers = "1.0.0" +sloggers = "1.0.1" types = { "path" = "../../consensus/types" } eth2_config = { "path" = "../../common/eth2_config" } task_executor = { "path" = "../../common/task_executor" } eth2_testnet_config = { path = "../../common/eth2_testnet_config" } logging = { path = "../../common/logging" } -slog-term = "2.5.0" +slog-term = "2.6.0" slog-async = "2.5.0" -ctrlc = { version = "3.1.4", features = ["termination"] } +ctrlc = { version = "3.1.6", features = ["termination"] } futures = "0.3.5" parking_lot = "0.11.0" slog-json = "2.3.0" diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 9d7dfd81c..0c701b9cd 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -13,16 +13,16 @@ fake_crypto = ["bls/fake_crypto"] [dependencies] bls = { path = "../../crypto/bls", default-features = false } compare_fields = { path = "../../common/compare_fields" } -ethereum-types = "0.9.1" +ethereum-types = "0.9.2" hex = "0.4.2" -rayon = "1.3.0" -serde = "1.0.110" -serde_derive = "1.0.110" -serde_repr = "0.1.5" -serde_yaml = "0.8.11" +rayon = "1.4.1" +serde = "1.0.116" +serde_derive = "1.0.116" +serde_repr = "0.1.6" +serde_yaml = "0.8.13" eth2_ssz = "0.1.2" eth2_ssz_derive = "0.1.0" -tree_hash = "0.1.0" +tree_hash = "0.1.1" tree_hash_derive = "0.2.0" cached_tree_hash = { path = "../../consensus/cached_tree_hash" } state_processing = { path = "../../consensus/state_processing" } diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml index cd0d1e858..d6a203b66 100644 --- a/testing/eth1_test_rig/Cargo.toml +++ b/testing/eth1_test_rig/Cargo.toml @@ -9,5 +9,5 @@ tokio = { version = "0.2.22", features = ["time"] } web3 = "0.11.0" futures = { version = "0.3.5", features = ["compat"] } types = { path = "../../consensus/types"} -serde_json = "1.0.52" +serde_json = "1.0.58" deposit_contract = { path = "../../common/deposit_contract"} diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index ae2393636..cfbd92620 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -10,9 +10,9 @@ beacon_node = { path = "../../beacon_node" } types = { path = "../../consensus/types" } eth2_config = { path = "../../common/eth2_config" } tempdir = "0.3.7" -reqwest = { version = "0.10.4", features = ["native-tls-vendored"] } +reqwest = { version = "0.10.8", features = ["native-tls-vendored"] } url = "2.1.1" -serde = "1.0.110" +serde = "1.0.116" futures = "0.3.5" genesis = { path = "../../beacon_node/genesis" } eth2 = { path = "../../common/eth2" } diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index 773d56bcb..22174ba7f 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -16,5 +16,5 @@ futures = "0.3.5" tokio = "0.2.22" eth1_test_rig = { path = "../eth1_test_rig" } env_logger = "0.7.1" -clap = "2.33.0" -rayon = "1.3.0" +clap = "2.33.3" +rayon = "1.4.1" diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index cf1e96bef..1557aa2b4 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -16,35 +16,35 @@ deposit_contract = { path = "../common/deposit_contract" } [dependencies] eth2_ssz = "0.1.2" eth2_config = { path = "../common/eth2_config" } -tree_hash = "0.1.0" -clap = "2.33.0" +tree_hash = "0.1.1" +clap = "2.33.3" eth2_interop_keypairs = { path = "../common/eth2_interop_keypairs" } slashing_protection = { path = "./slashing_protection" } slot_clock = { path = "../common/slot_clock" } types = { path = "../consensus/types" } -serde = "1.0.110" -serde_derive = "1.0.110" -serde_json = "1.0.52" +serde = "1.0.116" +serde_derive = "1.0.116" +serde_json = "1.0.58" serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } slog-async = "2.5.0" -slog-term = "2.5.0" +slog-term = "2.6.0" tokio = { version = "0.2.22", features = ["time"] } futures = { version = "0.3.5", features = ["compat"] } -dirs = "2.0.2" +dirs = "3.0.1" directory = {path = "../common/directory"} logging = { path = "../common/logging" } environment = { path = "../lighthouse/environment" } parking_lot = "0.11.0" exit-future = "0.2.0" -libc = "0.2.69" +libc = "0.2.79" eth2_ssz_derive = "0.1.0" hex = "0.4.2" deposit_contract = { path = "../common/deposit_contract" } bls = { path = "../crypto/bls" } eth2 = { path = "../common/eth2" } tempdir = "0.3.7" -rayon = "1.3.0" +rayon = "1.4.1" validator_dir = { path = "../common/validator_dir" } clap_utils = { path = "../common/clap_utils" } eth2_keystore = { path = "../crypto/eth2_keystore" } @@ -52,7 +52,7 @@ account_utils = { path = "../common/account_utils" } lighthouse_version = { path = "../common/lighthouse_version" } warp_utils = { path = "../common/warp_utils" } warp = "0.2.5" -hyper = "0.13.5" +hyper = "0.13.8" serde_utils = { path = "../consensus/serde_utils" } libsecp256k1 = "0.3.5" ring = "0.16.12" diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 6145826fd..a1abb8556 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" [dependencies] tempfile = "3.1.0" types = { path = "../../consensus/types" } -tree_hash = { path = "../../consensus/tree_hash" } -rusqlite = { version = "0.23.1", features = ["bundled"] } -r2d2 = "0.8.8" -r2d2_sqlite = "0.16.0" +tree_hash = "0.1.1" +rusqlite = { version = "0.24.0", features = ["bundled"] } +r2d2 = "0.8.9" +r2d2_sqlite = "0.17.0" parking_lot = "0.11.0" -serde = "1.0.110" -serde_derive = "1.0.110" -serde_json = "1.0.52" +serde = "1.0.116" +serde_derive = "1.0.116" +serde_json = "1.0.58" serde_utils = { path = "../../consensus/serde_utils" } [dev-dependencies] -rayon = "1.3.0" +rayon = "1.4.1" From da44821e39018a7b480f6ab3ef398776e63446bb Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Mon, 5 Oct 2020 21:08:14 +1100 Subject: [PATCH 24/32] Clean up obsolete TODOs (#1734) Squashed commit of the following: commit f99373cbaec9adb2bdbae3f7e903284327962083 Author: Age Manning <Age@AgeManning.com> Date: Mon Oct 5 18:44:09 2020 +1100 Clean up obsolute TODOs --- .../src/behaviour/handler/delegate.rs | 4 -- beacon_node/eth2_libp2p/src/behaviour/mod.rs | 12 +--- beacon_node/eth2_libp2p/src/discovery/enr.rs | 1 - .../eth2_libp2p/src/peer_manager/mod.rs | 71 +------------------ .../eth2_libp2p/src/peer_manager/peerdb.rs | 1 - beacon_node/eth2_libp2p/src/rpc/handler.rs | 4 -- .../network/src/attestation_service/mod.rs | 9 +-- beacon_node/network/src/service.rs | 5 +- beacon_node/network/src/sync/manager.rs | 4 +- .../network/src/sync/range_sync/chain.rs | 7 +- .../network/src/sync/range_sync/range.rs | 15 +++- 11 files changed, 21 insertions(+), 112 deletions(-) diff --git a/beacon_node/eth2_libp2p/src/behaviour/handler/delegate.rs b/beacon_node/eth2_libp2p/src/behaviour/handler/delegate.rs index 3ab8dcbec..452686a5c 100644 --- a/beacon_node/eth2_libp2p/src/behaviour/handler/delegate.rs +++ b/beacon_node/eth2_libp2p/src/behaviour/handler/delegate.rs @@ -54,8 +54,6 @@ impl<TSpec: EthSpec> DelegatingHandler<TSpec> { } } -// TODO: this can all be created with macros - /// Wrapper around the `ProtocolsHandler::InEvent` types of the handlers. /// Simply delegated to the corresponding behaviour's handler. #[derive(Debug, Clone)] @@ -115,7 +113,6 @@ pub type DelegateOutProto<TSpec> = EitherUpgrade< >, >; -// TODO: prob make this an enum pub type DelegateOutInfo<TSpec> = EitherOutput< <GossipHandler as ProtocolsHandler>::OutboundOpenInfo, EitherOutput< @@ -216,7 +213,6 @@ impl<TSpec: EthSpec> ProtocolsHandler for DelegatingHandler<TSpec> { <Self::OutboundProtocol as OutboundUpgrade<NegotiatedSubstream>>::Error, >, ) { - // TODO: find how to clean up match info { // Gossipsub EitherOutput::First(info) => match error { diff --git a/beacon_node/eth2_libp2p/src/behaviour/mod.rs b/beacon_node/eth2_libp2p/src/behaviour/mod.rs index 72d60a553..4c3f8e05c 100644 --- a/beacon_node/eth2_libp2p/src/behaviour/mod.rs +++ b/beacon_node/eth2_libp2p/src/behaviour/mod.rs @@ -102,7 +102,7 @@ pub struct Behaviour<TSpec: EthSpec> { /// The Eth2 RPC specified in the wire-0 protocol. eth2_rpc: RPC<TSpec>, /// Keep regular connection to peers and disconnect if absent. - // TODO: Using id for initial interop. This will be removed by mainnet. + // NOTE: The id protocol is used for initial interop. This will be removed by mainnet. /// Provides IP addresses and peer information. identify: Identify, /// The peer manager that keeps track of peer's reputation and status. @@ -203,9 +203,6 @@ impl<TSpec: EthSpec> Behaviour<TSpec> { self.enr_fork_id.fork_digest, ); - // TODO: Implement scoring - // let topic: Topic = gossip_topic.into(); - // self.gossipsub.set_topic_params(t.hash(), TopicScoreParams::default()); self.subscribe(gossip_topic) } @@ -227,12 +224,6 @@ impl<TSpec: EthSpec> Behaviour<TSpec> { GossipEncoding::default(), self.enr_fork_id.fork_digest, ); - // TODO: Implement scoring - /* - let t: Topic = topic.clone().into(); - self.gossipsub - .set_topic_params(t.hash(), TopicScoreParams::default()); - */ self.subscribe(topic) } @@ -620,7 +611,6 @@ impl<TSpec: EthSpec> Behaviour<TSpec> { RPCRequest::MetaData(_) => { // send the requested meta-data self.send_meta_data_response((handler_id, id), peer_id); - // TODO: inform the peer manager? } RPCRequest::Goodbye(reason) => { // queue for disconnection without a goodbye message diff --git a/beacon_node/eth2_libp2p/src/discovery/enr.rs b/beacon_node/eth2_libp2p/src/discovery/enr.rs index 853ea5f9a..ffe671dcf 100644 --- a/beacon_node/eth2_libp2p/src/discovery/enr.rs +++ b/beacon_node/eth2_libp2p/src/discovery/enr.rs @@ -129,7 +129,6 @@ pub fn create_enr_builder_from_config<T: EnrKey>(config: &NetworkConfig) -> EnrB builder.udp(udp_port); } // we always give it our listening tcp port - // TODO: Add uPnP support to map udp and tcp ports let tcp_port = config.enr_tcp_port.unwrap_or_else(|| config.libp2p_port); builder.tcp(tcp_port).tcp(config.libp2p_port); builder diff --git a/beacon_node/eth2_libp2p/src/peer_manager/mod.rs b/beacon_node/eth2_libp2p/src/peer_manager/mod.rs index 47dc23d7c..d03480d21 100644 --- a/beacon_node/eth2_libp2p/src/peer_manager/mod.rs +++ b/beacon_node/eth2_libp2p/src/peer_manager/mod.rs @@ -147,8 +147,7 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { /// /// If the peer doesn't exist, log a warning and insert defaults. pub fn report_peer(&mut self, peer_id: &PeerId, action: PeerAction) { - // TODO: Remove duplicate code - This is duplicated in the update_peer_scores() - // function. + // NOTE: This is duplicated in the update_peer_scores() and could be improved. // Variables to update the PeerDb if required. let mut ban_peer = None; @@ -179,7 +178,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { GoodbyeReason::BadScore, )); } - // TODO: Update the peer manager to inform that the peer is disconnecting. } ScoreState::Healthy => { debug!(self.log, "Peer transitioned to healthy state"; "peer_id" => peer_id.to_string(), "score" => info.score().to_string(), "past_state" => previous_state.to_string()); @@ -399,10 +397,7 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { // Not supporting a protocol shouldn't be considered a malicious action, but // it is an action that in some cases will make the peer unfit to continue // communicating. - // TODO: To avoid punishing a peer repeatedly for not supporting a protocol, this - // information could be stored and used to prevent sending requests for the given - // protocol to this peer. Similarly, to avoid blacklisting a peer for a protocol - // forever, if stored this information should expire. + match protocol { Protocol::Ping => PeerAction::Fatal, Protocol::BlocksByRange => return, @@ -436,7 +431,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { /// A ping request has been received. // NOTE: The behaviour responds with a PONG automatically - // TODO: Update last seen pub fn ping_request(&mut self, peer_id: &PeerId, seq: u64) { if let Some(peer_info) = self.network_globals.peers.read().peer_info(peer_id) { // received a ping @@ -466,7 +460,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { } /// A PONG has been returned from a peer. - // TODO: Update last seen pub fn pong_response(&mut self, peer_id: &PeerId, seq: u64) { if let Some(peer_info) = self.network_globals.peers.read().peer_info(peer_id) { // received a pong @@ -492,7 +485,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { } /// Received a metadata response from a peer. - // TODO: Update last seen pub fn meta_data_response(&mut self, peer_id: &PeerId, meta_data: MetaData<TSpec>) { if let Some(peer_info) = self.network_globals.peers.write().peer_info_mut(peer_id) { if let Some(known_meta_data) = &peer_info.meta_data { @@ -588,7 +580,7 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { let connected_or_dialing = self.network_globals.connected_or_dialing_peers(); for (peer_id, min_ttl) in results { // we attempt a connection if this peer is a subnet peer or if the max peer count - // is not yet filled (including dialling peers) + // is not yet filled (including dialing peers) if (min_ttl.is_some() || connected_or_dialing + to_dial_peers.len() < self.max_peers) && !self .network_globals @@ -601,7 +593,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { .read() .is_banned_or_disconnected(&peer_id) { - // TODO: Update output // This should be updated with the peer dialing. In fact created once the peer is // dialed if let Some(min_ttl) = min_ttl { @@ -690,58 +681,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { // Update scores info.score_update(); - /* TODO: Implement logic about connection lifetimes - match info.connection_status { - Connected { .. } => { - // Connected peers gain reputation by sending useful messages - } - Disconnected { since } | Banned { since } => { - // For disconnected peers, lower their reputation by 1 for every hour they - // stay disconnected. This helps us slowly forget disconnected peers. - // In the same way, slowly allow banned peers back again. - let dc_hours = now - .checked_duration_since(since) - .unwrap_or_else(|| Duration::from_secs(0)) - .as_secs() - / 3600; - let last_dc_hours = self - ._last_updated - .checked_duration_since(since) - .unwrap_or_else(|| Duration::from_secs(0)) - .as_secs() - / 3600; - if dc_hours > last_dc_hours { - // this should be 1 most of the time - let rep_dif = (dc_hours - last_dc_hours) - .try_into() - .unwrap_or(Rep::max_value()); - - info.reputation = if info.connection_status.is_banned() { - info.reputation.saturating_add(rep_dif) - } else { - info.reputation.saturating_sub(rep_dif) - }; - } - } - Dialing { since } => { - // A peer shouldn't be dialing for more than 2 minutes - if since.elapsed().as_secs() > 120 { - warn!(self.log,"Peer has been dialing for too long"; "peer_id" => id.to_string()); - // TODO: decide how to handle this - } - } - Unknown => {} //TODO: Handle this case - } - // Check if the peer gets banned or unbanned and if it should be disconnected - if info.reputation < _MIN_REP_BEFORE_BAN && !info.connection_status.is_banned() { - // This peer gets banned. Check if we should request disconnection - ban_queue.push(id.clone()); - } else if info.reputation >= _MIN_REP_BEFORE_BAN && info.connection_status.is_banned() { - // This peer gets unbanned - unban_queue.push(id.clone()); - } - */ - // handle score transitions if previous_state != info.score_state() { match info.score_state() { @@ -765,7 +704,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { GoodbyeReason::BadScore, )); } - // TODO: Update peer manager to report that it's disconnecting. } ScoreState::Healthy => { debug!(self.log, "Peer transitioned to healthy state"; "peer_id" => peer_id.to_string(), "score" => info.score().to_string(), "past_state" => previous_state.to_string()); @@ -829,9 +767,6 @@ impl<TSpec: EthSpec> PeerManager<TSpec> { /// /// NOTE: Discovery will only add a new query if one isn't already queued. fn heartbeat(&mut self) { - // TODO: Provide a back-off time for discovery queries. I.e Queue many initially, then only - // perform discoveries over a larger fixed interval. Perhaps one every 6 heartbeats. This - // is achievable with a leaky bucket let peer_count = self.network_globals.connected_or_dialing_peers(); if peer_count < self.target_peers { // If we need more peers, queue a discovery lookup. diff --git a/beacon_node/eth2_libp2p/src/peer_manager/peerdb.rs b/beacon_node/eth2_libp2p/src/peer_manager/peerdb.rs index 0f8774f7c..184baa5b2 100644 --- a/beacon_node/eth2_libp2p/src/peer_manager/peerdb.rs +++ b/beacon_node/eth2_libp2p/src/peer_manager/peerdb.rs @@ -130,7 +130,6 @@ impl<TSpec: EthSpec> PeerDB<TSpec> { } /// Returns a mutable reference to a peer's info if known. - /// TODO: make pub(super) to ensure that peer management is unified pub fn peer_info_mut(&mut self, peer_id: &PeerId) -> Option<&mut PeerInfo<TSpec>> { self.peers.get_mut(peer_id) } diff --git a/beacon_node/eth2_libp2p/src/rpc/handler.rs b/beacon_node/eth2_libp2p/src/rpc/handler.rs index a4b18b03b..93f26eed8 100644 --- a/beacon_node/eth2_libp2p/src/rpc/handler.rs +++ b/beacon_node/eth2_libp2p/src/rpc/handler.rs @@ -25,8 +25,6 @@ use std::{ use tokio::time::{delay_queue, delay_until, Delay, DelayQueue, Instant as TInstant}; use types::EthSpec; -//TODO: Implement check_timeout() on the substream types - /// The time (in seconds) before a substream that is awaiting a response from the user times out. pub const RESPONSE_TIMEOUT: u64 = 10; @@ -163,8 +161,6 @@ struct OutboundInfo<TSpec: EthSpec> { /// Info over the protocol this substream is handling. proto: Protocol, /// Number of chunks to be seen from the peer's response. - // TODO: removing the option could allow clossing the streams after the number of - // expected responses is met for all protocols. remaining_chunks: Option<u64>, /// `RequestId` as given by the application that sent the request. req_id: RequestId, diff --git a/beacon_node/network/src/attestation_service/mod.rs b/beacon_node/network/src/attestation_service/mod.rs index 7c017d295..cd8a9b5a1 100644 --- a/beacon_node/network/src/attestation_service/mod.rs +++ b/beacon_node/network/src/attestation_service/mod.rs @@ -195,14 +195,9 @@ impl<T: BeaconChainTypes> AttestationService<T> { slot: subscription.slot, }; - // determine if the validator is an aggregator. If so, we subscribe to the subnet and + // Determine if the validator is an aggregator. If so, we subscribe to the subnet and // if successful add the validator to a mapping of known aggregators for that exact // subnet. - // NOTE: There is a chance that a fork occurs between now and when the validator needs - // to aggregate attestations. If this happens, the signature will no longer be valid - // and it could be likely the validator no longer needs to aggregate. More - // sophisticated logic should be added using known future forks. - // TODO: Implement if subscription.is_aggregator { metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); @@ -286,8 +281,6 @@ impl<T: BeaconChainTypes> AttestationService<T> { min_ttl, }) } else { - // TODO: Send the time frame needed to have a peer connected, so that we can - // maintain peers for a least this duration. // We may want to check the global PeerInfo to see estimated timeouts for each // peer before they can be removed. warn!(self.log, diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index ec86696b3..1188211ef 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -51,7 +51,7 @@ pub enum NetworkMessage<T: EthSpec> { }, /// Respond to a peer's request with an error. SendError { - // TODO: note that this is never used, we just say goodbye without nicely closing the + // NOTE: Currently this is never used, we just say goodbye without nicely closing the // stream assigned to the request peer_id: PeerId, error: RPCResponseErrorCode, @@ -163,7 +163,7 @@ impl<T: BeaconChainTypes> NetworkService<T> { "Loading peers into the routing table"; "peers" => enrs_to_load.len() ); for enr in enrs_to_load { - libp2p.swarm.add_enr(enr.clone()); //TODO change? + libp2p.swarm.add_enr(enr.clone()); } // launch derived network services @@ -349,7 +349,6 @@ fn spawn_service<T: BeaconChainTypes>( // process any attestation service events Some(attestation_service_message) = service.attestation_service.next() => { match attestation_service_message { - // TODO: Implement AttServiceMessage::Subscribe(subnet_id) => { service.libp2p.swarm.subscribe_to_subnet(subnet_id); } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 8f4d15c5c..4f59c6cff 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -119,7 +119,6 @@ pub enum SyncMessage<T: EthSpec> { } /// The result of processing a multiple blocks (a chain segment). -// TODO: When correct batch error handling occurs, we will include an error type. #[derive(Debug)] pub enum BatchProcessResult { /// The batch was completed successfully. It carries whether the sent batch contained blocks. @@ -629,7 +628,7 @@ impl<T: BeaconChainTypes> SyncManager<T> { self.update_sync_state(); } - // TODO: Group these functions into one. + // TODO: Group these functions into one for cleaner code. /// Updates the syncing state of a peer to be synced. fn synced_peer(&mut self, peer_id: &PeerId, sync_info: PeerSyncInfo) { if let Some(peer_info) = self.network_globals.peers.write().peer_info_mut(peer_id) { @@ -792,7 +791,6 @@ impl<T: BeaconChainTypes> SyncManager<T> { // This currently can be a host of errors. We permit this due to the partial // ambiguity. - // TODO: Refine the error types and score the peer appropriately. self.network.report_peer( parent_request.last_submitted_peer, PeerAction::MidToleranceError, diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 8ed21616d..864ac6124 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -613,9 +613,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { BatchState::Failed | BatchState::Poisoned | BatchState::AwaitingDownload => { unreachable!("batch indicates inconsistent chain state while advancing chain") } - BatchState::AwaitingProcessing(..) => { - // TODO: can we be sure the old attempts are wrong? - } + BatchState::AwaitingProcessing(..) => {} BatchState::Processing(_) => { assert_eq!( id, @@ -651,9 +649,6 @@ impl<T: BeaconChainTypes> SyncingChain<T> { /// These events occur when a peer has successfully responded with blocks, but the blocks we /// have received are incorrect or invalid. This indicates the peer has not performed as /// intended and can result in downvoting a peer. - // TODO: Batches could have been partially downloaded due to RPC size-limit restrictions. We - // need to add logic for partial batch downloads. Potentially, if another peer returns the same - // batch, we try a partial download. fn handle_invalid_batch( &mut self, network: &mut SyncNetworkContext<T::EthSpec>, diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 48a9bd5d4..c1ae653a7 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -220,7 +220,10 @@ impl<T: BeaconChainTypes> RangeSync<T> { if let Some(removed_chain) = removed_chain { debug!(self.log, "Chain removed after block response"; "sync_type" => ?sync_type, "chain_id" => chain_id); removed_chain.status_peers(network); - // TODO: update & update_sync_state? + // update the state of the collection + self.chains.update(network); + // update the global state and inform the user + self.chains.update_sync_state(network); } } Err(_) => { @@ -319,7 +322,10 @@ impl<T: BeaconChainTypes> RangeSync<T> { .call_all(|chain| chain.remove_peer(peer_id, network)) { debug!(self.log, "Chain removed after removing peer"; "sync_type" => ?sync_type, "chain" => removed_chain.get_id()); - // TODO: anything else to do? + // update the state of the collection + self.chains.update(network); + // update the global state and inform the user + self.chains.update_sync_state(network); } } @@ -343,7 +349,10 @@ impl<T: BeaconChainTypes> RangeSync<T> { if let Some(removed_chain) = removed_chain { debug!(self.log, "Chain removed on rpc error"; "sync_type" => ?sync_type, "chain" => removed_chain.get_id()); removed_chain.status_peers(network); - // TODO: update & update_sync_state? + // update the state of the collection + self.chains.update(network); + // update the global state and inform the user + self.chains.update_sync_state(network); } } Err(_) => { From 59adc5ba00045a584e5b9bbbc3677f1a038f837d Mon Sep 17 00:00:00 2001 From: blacktemplar <blacktemplar@a1.net> Date: Mon, 5 Oct 2020 10:50:43 +0000 Subject: [PATCH 25/32] Implement key cache to reduce keystore loading times for validator_client (#1695) ## Issue Addressed #1618 ## Proposed Changes Adds an encrypted key cache that is loaded on validator_client startup. It stores the keypairs for all enabled keystores and uses as password the concatenation the passwords of all enabled keystores. This reduces the number of time intensive key derivitions for `N` validators from `N` to `1`. On changes the cache gets updated asynchronously to avoid blocking the main thread. ## Additional Info If the cache contains the keypair of a keystore that is not in the validator_definitions.yml file during loading the cache cannot get decrypted. In this case all the keystores get decrypted and then the cache gets overwritten. To avoid that one can disable keystores in validator_definitions.yml and restart the client which will remove them from the cache, after that one can entirely remove the keystore (from the validator_definitions.yml and from the disk). Other solutions to the above "problem" might be: * Add a CLI and/or API function for removing keystores which will update the cache (asynchronously). * Add a CLI and/or API function that just updates the cache (asynchronously) after a modification of the `validator_definitions.yml` file. Note that the cache file has a lock file which gets removed immediatly after the cache was used or updated. --- Cargo.lock | 44 ++- crypto/bls/src/zeroize_hash.rs | 4 +- validator_client/Cargo.toml | 2 + .../src/initialized_validators.rs | 366 +++++++++++++----- validator_client/src/key_cache.rs | 347 +++++++++++++++++ validator_client/src/lib.rs | 1 + 6 files changed, 667 insertions(+), 97 deletions(-) create mode 100644 validator_client/src/key_cache.rs diff --git a/Cargo.lock b/Cargo.lock index 9a44c5f55..878ee6946 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,16 @@ dependencies = [ "types", ] +[[package]] +name = "bincode" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "bitflags" version = "0.9.1" @@ -1632,7 +1642,7 @@ dependencies = [ "hmac 0.9.0", "pbkdf2 0.5.0", "rand 0.7.3", - "scrypt", + "scrypt 0.4.1", "serde", "serde_json", "serde_repr", @@ -2347,6 +2357,16 @@ dependencies = [ "digest 0.8.1", ] +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.9.0" @@ -3933,6 +3953,15 @@ dependencies = [ "crypto-mac 0.7.0", ] +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + [[package]] name = "pbkdf2" version = "0.5.0" @@ -4761,6 +4790,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scrypt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e7e75e27e8cd47e4be027d4b9fdc0b696116f981c22de21ca7bad63a9cb33a" +dependencies = [ + "hmac 0.8.1", + "pbkdf2 0.4.0", + "sha2 0.9.1", +] + [[package]] name = "scrypt" version = "0.4.1" @@ -6356,6 +6396,7 @@ name = "validator_client" version = "0.2.13" dependencies = [ "account_utils", + "bincode", "bls", "clap", "clap_utils", @@ -6381,6 +6422,7 @@ dependencies = [ "rand 0.7.3", "rayon", "ring", + "scrypt 0.3.1", "serde", "serde_derive", "serde_json", diff --git a/crypto/bls/src/zeroize_hash.rs b/crypto/bls/src/zeroize_hash.rs index 3d81df1d8..41136f97a 100644 --- a/crypto/bls/src/zeroize_hash.rs +++ b/crypto/bls/src/zeroize_hash.rs @@ -1,9 +1,11 @@ use super::SECRET_KEY_BYTES_LEN; +use serde_derive::{Deserialize, Serialize}; use zeroize::Zeroize; /// Provides a wrapper around a `[u8; SECRET_KEY_BYTES_LEN]` that implements `Zeroize` on `Drop`. -#[derive(Zeroize)] +#[derive(Zeroize, Serialize, Deserialize)] #[zeroize(drop)] +#[serde(transparent)] pub struct ZeroizeHash([u8; SECRET_KEY_BYTES_LEN]); impl ZeroizeHash { diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 1557aa2b4..a4035b731 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -24,6 +24,7 @@ slot_clock = { path = "../common/slot_clock" } types = { path = "../consensus/types" } serde = "1.0.116" serde_derive = "1.0.116" +bincode = "1.3.1" serde_json = "1.0.58" serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } @@ -57,3 +58,4 @@ serde_utils = { path = "../consensus/serde_utils" } libsecp256k1 = "0.3.5" ring = "0.16.12" rand = "0.7.3" +scrypt = { version = "0.3.0", default-features = false } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index dbab008e8..09fd2ae9d 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -11,15 +11,19 @@ use account_utils::{ validator_definitions::{ self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME, }, + ZeroizeString, }; use eth2_keystore::Keystore; -use slog::{error, info, warn, Logger}; -use std::collections::HashMap; +use slog::{debug, error, info, warn, Logger}; +use std::collections::{HashMap, HashSet}; use std::fs::{self, File, OpenOptions}; use std::io; use std::path::PathBuf; use types::{Keypair, PublicKey}; +use crate::key_cache; +use crate::key_cache::KeyCache; + // Use TTY instead of stdin to capture passwords from users. const USE_STDIN: bool = false; @@ -37,9 +41,11 @@ pub enum Error { }, /// There was a filesystem error when opening the keystore. UnableToOpenVotingKeystore(io::Error), + UnableToOpenKeyCache(key_cache::Error), /// The keystore path is not as expected. It should be a file, not `..` or something obscure /// like that. BadVotingKeystorePath(PathBuf), + BadKeyCachePath(PathBuf), /// The keystore could not be parsed, it is likely bad JSON. UnableToParseVotingKeystore(eth2_keystore::Error), /// The keystore could not be decrypted. The password might be wrong. @@ -79,6 +85,59 @@ pub struct InitializedValidator { signing_method: SigningMethod, } +fn open_keystore(path: &PathBuf) -> Result<Keystore, Error> { + let keystore_file = File::open(path).map_err(Error::UnableToOpenVotingKeystore)?; + Keystore::from_json_reader(keystore_file).map_err(Error::UnableToParseVotingKeystore) +} + +fn get_lockfile_path(file_path: &PathBuf) -> Option<PathBuf> { + file_path + .file_name() + .and_then(|os_str| os_str.to_str()) + .map(|filename| { + file_path + .clone() + .with_file_name(format!("{}.lock", filename)) + }) +} + +fn create_lock_file( + file_path: &PathBuf, + delete_lockfiles: bool, + log: &Logger, +) -> Result<(), Error> { + if file_path.exists() { + if delete_lockfiles { + warn!( + log, + "Deleting validator lockfile"; + "file" => format!("{:?}", file_path) + ); + + fs::remove_file(file_path).map_err(Error::UnableToDeleteLockfile)?; + } else { + return Err(Error::LockfileExists(file_path.clone())); + } + } + // Create a new lockfile. + OpenOptions::new() + .write(true) + .create_new(true) + .open(file_path) + .map_err(Error::UnableToCreateLockfile)?; + Ok(()) +} + +fn remove_lock(lock_path: &PathBuf) { + if lock_path.exists() { + if let Err(e) = fs::remove_file(&lock_path) { + eprintln!("Failed to remove {:?}: {:?}", lock_path, e) + } + } else { + eprintln!("Lockfile missing: {:?}", lock_path) + } +} + impl InitializedValidator { /// Instantiate `self` from a `ValidatorDefinition`. /// @@ -88,10 +147,12 @@ impl InitializedValidator { /// ## Errors /// /// If the validator is unable to be initialized for whatever reason. - pub fn from_definition( + async fn from_definition( def: ValidatorDefinition, delete_lockfiles: bool, log: &Logger, + key_cache: &mut KeyCache, + key_stores: &mut HashMap<PathBuf, Keystore>, ) -> Result<Self, Error> { if !def.enabled { return Err(Error::UnableToInitializeDisabledValidator); @@ -105,30 +166,55 @@ impl InitializedValidator { voting_keystore_password_path, voting_keystore_password, } => { - let keystore_file = - File::open(&voting_keystore_path).map_err(Error::UnableToOpenVotingKeystore)?; - let voting_keystore = Keystore::from_json_reader(keystore_file) - .map_err(Error::UnableToParseVotingKeystore)?; + use std::collections::hash_map::Entry::*; + let voting_keystore = match key_stores.entry(voting_keystore_path.clone()) { + Vacant(entry) => entry.insert(open_keystore(&voting_keystore_path)?), + Occupied(entry) => entry.into_mut(), + }; - let voting_keypair = match (voting_keystore_password_path, voting_keystore_password) - { - // If the password is supplied, use it and ignore the path (if supplied). - (_, Some(password)) => voting_keystore - .decrypt_keypair(password.as_ref()) - .map_err(Error::UnableToDecryptKeystore)?, - // If only the path is supplied, use the path. - (Some(path), None) => { - let password = read_password(path) - .map_err(Error::UnableToReadVotingKeystorePassword)?; - - voting_keystore - .decrypt_keypair(password.as_bytes()) - .map_err(Error::UnableToDecryptKeystore)? - } - // If there is no password available, maybe prompt for a password. - (None, None) => { - unlock_keystore_via_stdin_password(&voting_keystore, &voting_keystore_path)? - } + let voting_keypair = if let Some(keypair) = key_cache.get(voting_keystore.uuid()) { + keypair + } else { + let keystore = voting_keystore.clone(); + let keystore_path = voting_keystore_path.clone(); + // Decoding a local keystore can take several seconds, therefore it's best + // to keep if off the core executor. This also has the fortunate effect of + // interrupting the potentially long-running task during shut down. + let (password, keypair) = tokio::task::spawn_blocking(move || { + Ok( + match (voting_keystore_password_path, voting_keystore_password) { + // If the password is supplied, use it and ignore the path + // (if supplied). + (_, Some(password)) => ( + password.as_ref().to_vec().into(), + keystore + .decrypt_keypair(password.as_ref()) + .map_err(Error::UnableToDecryptKeystore)?, + ), + // If only the path is supplied, use the path. + (Some(path), None) => { + let password = read_password(path) + .map_err(Error::UnableToReadVotingKeystorePassword)?; + let keypair = keystore + .decrypt_keypair(password.as_bytes()) + .map_err(Error::UnableToDecryptKeystore)?; + (password, keypair) + } + // If there is no password available, maybe prompt for a password. + (None, None) => { + let (password, keypair) = unlock_keystore_via_stdin_password( + &keystore, + &keystore_path, + )?; + (password.as_ref().to_vec().into(), keypair) + } + }, + ) + }) + .await + .map_err(Error::TokioJoin)??; + key_cache.add(keypair.clone(), voting_keystore.uuid(), password); + keypair }; if voting_keypair.pk != def.voting_public_key { @@ -139,47 +225,16 @@ impl InitializedValidator { } // Append a `.lock` suffix to the voting keystore. - let voting_keystore_lockfile_path = voting_keystore_path - .file_name() - .ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone())) - .and_then(|os_str| { - os_str.to_str().ok_or_else(|| { - Error::BadVotingKeystorePath(voting_keystore_path.clone()) - }) - }) - .map(|filename| { - voting_keystore_path - .clone() - .with_file_name(format!("{}.lock", filename)) - })?; + let voting_keystore_lockfile_path = get_lockfile_path(&voting_keystore_path) + .ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone()))?; - if voting_keystore_lockfile_path.exists() { - if delete_lockfiles { - warn!( - log, - "Deleting validator lockfile"; - "file" => format!("{:?}", voting_keystore_lockfile_path) - ); - - fs::remove_file(&voting_keystore_lockfile_path) - .map_err(Error::UnableToDeleteLockfile)?; - } else { - return Err(Error::LockfileExists(voting_keystore_lockfile_path)); - } - } else { - // Create a new lockfile. - OpenOptions::new() - .write(true) - .create_new(true) - .open(&voting_keystore_lockfile_path) - .map_err(Error::UnableToCreateLockfile)?; - } + create_lock_file(&voting_keystore_lockfile_path, delete_lockfiles, &log)?; Ok(Self { signing_method: SigningMethod::LocalKeystore { voting_keystore_path, voting_keystore_lockfile_path, - voting_keystore, + voting_keystore: voting_keystore.clone(), voting_keypair, }, }) @@ -210,16 +265,7 @@ impl Drop for InitializedValidator { voting_keystore_lockfile_path, .. } => { - if voting_keystore_lockfile_path.exists() { - if let Err(e) = fs::remove_file(&voting_keystore_lockfile_path) { - eprintln!( - "Failed to remove {:?}: {:?}", - voting_keystore_lockfile_path, e - ) - } - } else { - eprintln!("Lockfile missing: {:?}", voting_keystore_lockfile_path) - } + remove_lock(voting_keystore_lockfile_path); } } } @@ -229,7 +275,7 @@ impl Drop for InitializedValidator { fn unlock_keystore_via_stdin_password( keystore: &Keystore, keystore_path: &PathBuf, -) -> Result<Keypair, Error> { +) -> Result<(ZeroizeString, Keypair), Error> { eprintln!(""); eprintln!( "The {} file does not contain either of the following fields for {:?}:", @@ -255,7 +301,7 @@ fn unlock_keystore_via_stdin_password( eprintln!(""); match keystore.decrypt_keypair(password.as_ref()) { - Ok(keystore) => break Ok(keystore), + Ok(keystore) => break Ok((password, keystore)), Err(eth2_keystore::Error::InvalidPassword) => { eprintln!("Invalid password, try again (or press Ctrl+c to exit):"); } @@ -269,9 +315,8 @@ fn unlock_keystore_via_stdin_password( /// /// Forms the fundamental list of validators that are managed by this validator client instance. pub struct InitializedValidators { - /// If `true`, no validator will be opened if a lockfile exists. If `false`, a warning will be - /// raised for an existing lockfile, but it will ultimately be ignored. - strict_lockfiles: bool, + /// If `true`, delete any validator keystore lockfiles that would prevent starting. + delete_lockfiles: bool, /// A list of validator definitions which can be stored on-disk. definitions: ValidatorDefinitions, /// The directory that the `self.definitions` will be saved into. @@ -287,11 +332,11 @@ impl InitializedValidators { pub async fn from_definitions( definitions: ValidatorDefinitions, validators_dir: PathBuf, - strict_lockfiles: bool, + delete_lockfiles: bool, log: Logger, ) -> Result<Self, Error> { let mut this = Self { - strict_lockfiles, + delete_lockfiles, validators_dir, definitions, validators: HashMap::default(), @@ -393,6 +438,84 @@ impl InitializedValidators { Ok(()) } + /// Tries to decrypt the key cache. + /// + /// Returns `Ok(true)` if decryption was successful, `Ok(false)` if it couldn't get decrypted + /// and an error if a needed password couldn't get extracted. + /// + async fn decrypt_key_cache( + &self, + mut cache: KeyCache, + key_stores: &mut HashMap<PathBuf, Keystore>, + ) -> Result<KeyCache, Error> { + //read relevant key_stores + let mut definitions_map = HashMap::new(); + for def in self.definitions.as_slice() { + match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => { + use std::collections::hash_map::Entry::*; + let key_store = match key_stores.entry(voting_keystore_path.clone()) { + Vacant(entry) => entry.insert(open_keystore(voting_keystore_path)?), + Occupied(entry) => entry.into_mut(), + }; + definitions_map.insert(*key_store.uuid(), def); + } + } + } + + //check if all paths are in the definitions_map + for uuid in cache.uuids() { + if !definitions_map.contains_key(uuid) { + warn!( + self.log, + "Unknown uuid in cache"; + "uuid" => format!("{}", uuid) + ); + return Ok(KeyCache::new()); + } + } + + //collect passwords + let mut passwords = Vec::new(); + let mut public_keys = Vec::new(); + for uuid in cache.uuids() { + let def = definitions_map.get(uuid).expect("Existence checked before"); + let pw = match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_password_path, + voting_keystore_password, + voting_keystore_path, + } => { + if let Some(p) = voting_keystore_password { + p.as_ref().to_vec().into() + } else if let Some(path) = voting_keystore_password_path { + read_password(path).map_err(Error::UnableToReadVotingKeystorePassword)? + } else { + let keystore = open_keystore(voting_keystore_path)?; + unlock_keystore_via_stdin_password(&keystore, &voting_keystore_path)? + .0 + .as_ref() + .to_vec() + .into() + } + } + }; + passwords.push(pw); + public_keys.push(def.voting_public_key.clone()); + } + + //decrypt + tokio::task::spawn_blocking(move || match cache.decrypt(passwords, public_keys) { + Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => cache, + _ => KeyCache::new(), + }) + .await + .map_err(Error::TokioJoin) + } + /// Scans `self.definitions` and attempts to initialize and validators which are not already /// initialized. /// @@ -405,31 +528,48 @@ impl InitializedValidators { /// I.e., if there are two different definitions with the same public key then the second will /// be ignored. async fn update_validators(&mut self) -> Result<(), Error> { + //use key cache if available + let mut key_stores = HashMap::new(); + + // Create a lock file for the cache + let key_cache_path = KeyCache::cache_file_path(&self.validators_dir); + let cache_lockfile_path = get_lockfile_path(&key_cache_path) + .ok_or_else(|| Error::BadKeyCachePath(key_cache_path))?; + create_lock_file(&cache_lockfile_path, self.delete_lockfiles, &self.log)?; + + let mut key_cache = self + .decrypt_key_cache( + KeyCache::open_or_create(&self.validators_dir) + .map_err(Error::UnableToOpenKeyCache)?, + &mut key_stores, + ) + .await?; + + let mut disabled_uuids = HashSet::new(); for def in self.definitions.as_slice() { if def.enabled { match &def.signing_definition { - SigningDefinition::LocalKeystore { .. } => { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => { if self.validators.contains_key(&def.voting_public_key) { continue; } - // Decoding a local keystore can take several seconds, therefore it's best - // to keep if off the core executor. This also has the fortunate effect of - // interrupting the potentially long-running task during shut down. - let inner_def = def.clone(); - let strict_lockfiles = self.strict_lockfiles; - let inner_log = self.log.clone(); - let result = tokio::task::spawn_blocking(move || { - InitializedValidator::from_definition( - inner_def, - strict_lockfiles, - &inner_log, - ) - }) - .await - .map_err(Error::TokioJoin)?; + if let Some(key_store) = key_stores.get(voting_keystore_path) { + disabled_uuids.remove(key_store.uuid()); + } - match result { + match InitializedValidator::from_definition( + def.clone(), + self.delete_lockfiles, + &self.log, + &mut key_cache, + &mut key_stores, + ) + .await + { Ok(init) => { self.validators .insert(init.voting_public_key().clone(), init); @@ -455,6 +595,17 @@ impl InitializedValidators { } } else { self.validators.remove(&def.voting_public_key); + match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => { + if let Some(key_store) = key_stores.get(voting_keystore_path) { + disabled_uuids.insert(*key_store.uuid()); + } + } + } + info!( self.log, "Disabled validator"; @@ -462,6 +613,31 @@ impl InitializedValidators { ); } } + for uuid in disabled_uuids { + key_cache.remove(&uuid); + } + + let validators_dir = self.validators_dir.clone(); + let log = self.log.clone(); + if key_cache.is_modified() { + tokio::task::spawn_blocking(move || { + match key_cache.save(validators_dir) { + Err(e) => warn!( + log, + "Error during saving of key_cache"; + "err" => format!("{:?}", e) + ), + Ok(true) => info!(log, "Modified key_cache saved successfully"), + _ => {} + }; + remove_lock(&cache_lockfile_path); + }) + .await + .map_err(Error::TokioJoin)?; + } else { + debug!(log, "Key cache not modified"); + remove_lock(&cache_lockfile_path); + } Ok(()) } } diff --git a/validator_client/src/key_cache.rs b/validator_client/src/key_cache.rs new file mode 100644 index 000000000..6da06aaa1 --- /dev/null +++ b/validator_client/src/key_cache.rs @@ -0,0 +1,347 @@ +use account_utils::create_with_600_perms; +use bls::{Keypair, PublicKey}; +use eth2_keystore::json_keystore::{ + Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, KdfModule, + Sha256Checksum, +}; +use eth2_keystore::{ + decrypt, default_kdf, encrypt, keypair_from_secret, Error as KeystoreError, PlainText, Uuid, + ZeroizeHash, IV_SIZE, SALT_SIZE, +}; +use rand::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::OpenOptions; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// The file name for the serialized `KeyCache` struct. +pub const CACHE_FILENAME: &str = "validator_key_cache.json"; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum State { + NotDecrypted, + DecryptedAndSaved, + DecryptedWithUnsavedUpdates, +} + +fn not_decrypted() -> State { + State::NotDecrypted +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct KeyCache { + crypto: Crypto, + uuids: Vec<Uuid>, + #[serde(skip)] + pairs: HashMap<Uuid, Keypair>, //maps public keystore uuids to their corresponding Keypair + #[serde(skip)] + passwords: Vec<PlainText>, + #[serde(skip)] + #[serde(default = "not_decrypted")] + state: State, +} + +type SerializedKeyMap = HashMap<Uuid, ZeroizeHash>; + +impl KeyCache { + pub fn new() -> Self { + KeyCache { + uuids: Vec::new(), + crypto: Self::init_crypto(), + pairs: HashMap::new(), + passwords: Vec::new(), + state: State::DecryptedWithUnsavedUpdates, + } + } + + pub fn init_crypto() -> Crypto { + let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>(); + let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into(); + + let kdf = default_kdf(salt.to_vec()); + let cipher = Cipher::Aes128Ctr(Aes128Ctr { iv }); + + Crypto { + kdf: KdfModule { + function: kdf.function(), + params: kdf, + message: EmptyString, + }, + checksum: ChecksumModule { + function: Sha256Checksum::function(), + params: EmptyMap, + message: Vec::new().into(), + }, + cipher: CipherModule { + function: cipher.function(), + params: cipher, + message: Vec::new().into(), + }, + } + } + + pub fn cache_file_path<P: AsRef<Path>>(validators_dir: P) -> PathBuf { + validators_dir.as_ref().join(CACHE_FILENAME) + } + + /// Open an existing file or create a new, empty one if it does not exist. + pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> { + let cache_path = Self::cache_file_path(validators_dir.as_ref()); + if !cache_path.exists() { + Ok(Self::new()) + } else { + Self::open(validators_dir) + } + } + + /// Open an existing file, returning an error if the file does not exist. + pub fn open<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> { + let cache_path = validators_dir.as_ref().join(CACHE_FILENAME); + let file = OpenOptions::new() + .read(true) + .create_new(false) + .open(&cache_path) + .map_err(Error::UnableToOpenFile)?; + serde_json::from_reader(file).map_err(Error::UnableToParseFile) + } + + fn encrypt(&mut self) -> Result<(), Error> { + self.crypto = Self::init_crypto(); + let secret_map: SerializedKeyMap = self + .pairs + .iter() + .map(|(k, v)| (*k, v.sk.serialize())) + .collect(); + + let raw = PlainText::from( + bincode::serialize(&secret_map).map_err(Error::UnableToSerializeKeyMap)?, + ); + let (cipher_text, checksum) = encrypt( + raw.as_ref(), + Self::password(&self.passwords).as_ref(), + &self.crypto.kdf.params, + &self.crypto.cipher.params, + ) + .map_err(Error::UnableToEncrypt)?; + + self.crypto.cipher.message = cipher_text.into(); + self.crypto.checksum.message = checksum.to_vec().into(); + Ok(()) + } + + /// Stores `Self` encrypted in json format. + /// + /// Will create a new file if it does not exist or over-write any existing file. + /// Returns false iff there are no unsaved changes + pub fn save<P: AsRef<Path>>(&mut self, validators_dir: P) -> Result<bool, Error> { + if self.is_modified() { + self.encrypt()?; + + let cache_path = validators_dir.as_ref().join(CACHE_FILENAME); + let bytes = serde_json::to_vec(self).map_err(Error::UnableToEncodeFile)?; + + let res = if cache_path.exists() { + fs::write(cache_path, &bytes).map_err(Error::UnableToWriteFile) + } else { + create_with_600_perms(&cache_path, &bytes).map_err(Error::UnableToWriteFile) + }; + if res.is_ok() { + self.state = State::DecryptedAndSaved; + } + res.map(|_| true) + } else { + Ok(false) + } + } + + pub fn is_modified(&self) -> bool { + self.state == State::DecryptedWithUnsavedUpdates + } + + pub fn uuids(&self) -> &Vec<Uuid> { + &self.uuids + } + + fn password(passwords: &[PlainText]) -> PlainText { + PlainText::from(passwords.iter().fold(Vec::new(), |mut v, p| { + v.extend(p.as_ref()); + v + })) + } + + pub fn decrypt( + &mut self, + passwords: Vec<PlainText>, + public_keys: Vec<PublicKey>, + ) -> Result<&HashMap<Uuid, Keypair>, Error> { + match self.state { + State::NotDecrypted => { + let password = Self::password(&passwords); + let text = + decrypt(password.as_ref(), &self.crypto).map_err(Error::UnableToDecrypt)?; + let key_map: SerializedKeyMap = + bincode::deserialize(text.as_bytes()).map_err(Error::UnableToParseKeyMap)?; + self.passwords = passwords; + self.pairs = HashMap::new(); + if public_keys.len() != self.uuids.len() { + return Err(Error::PublicKeyMismatch); + } + for (uuid, public_key) in self.uuids.iter().zip(public_keys.iter()) { + if let Some(secret) = key_map.get(uuid) { + let key_pair = keypair_from_secret(secret.as_ref()) + .map_err(Error::UnableToParseKeyPair)?; + if &key_pair.pk != public_key { + return Err(Error::PublicKeyMismatch); + } + self.pairs.insert(*uuid, key_pair); + } else { + return Err(Error::MissingUuidKey); + } + } + self.state = State::DecryptedAndSaved; + Ok(&self.pairs) + } + _ => Err(Error::AlreadyDecrypted), + } + } + + pub fn remove(&mut self, uuid: &Uuid) { + //do nothing in not decrypted state + if let State::NotDecrypted = self.state { + return; + } + self.pairs.remove(uuid); + if let Some(pos) = self.uuids.iter().position(|uuid2| uuid2 == uuid) { + self.uuids.remove(pos); + self.passwords.remove(pos); + } + self.state = State::DecryptedWithUnsavedUpdates; + } + + pub fn add(&mut self, keypair: Keypair, uuid: &Uuid, password: PlainText) { + //do nothing in not decrypted state + if let State::NotDecrypted = self.state { + return; + } + self.pairs.insert(*uuid, keypair); + self.uuids.push(*uuid); + self.passwords.push(password); + self.state = State::DecryptedWithUnsavedUpdates; + } + + pub fn get(&self, uuid: &Uuid) -> Option<Keypair> { + self.pairs.get(uuid).cloned() + } +} + +#[derive(Debug)] +pub enum Error { + /// The cache file could not be opened. + UnableToOpenFile(io::Error), + /// The cache file could not be parsed as JSON. + UnableToParseFile(serde_json::Error), + /// The cache file could not be serialized as YAML. + UnableToEncodeFile(serde_json::Error), + /// The cache file could not be written to the filesystem. + UnableToWriteFile(io::Error), + /// Couldn't decrypt the cache file + UnableToDecrypt(KeystoreError), + UnableToEncrypt(KeystoreError), + /// Couldn't decode the decrypted hashmap + UnableToParseKeyMap(bincode::Error), + UnableToParseKeyPair(KeystoreError), + UnableToSerializeKeyMap(bincode::Error), + PublicKeyMismatch, + MissingUuidKey, + /// Cache file is already decrypted + AlreadyDecrypted, +} + +#[cfg(test)] +mod tests { + use super::*; + use eth2_keystore::json_keystore::{HexBytes, Kdf}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct KeyCacheTest { + pub params: Kdf, + //pub checksum: ChecksumModule, + //pub cipher: CipherModule, + uuids: Vec<Uuid>, + } + + #[tokio::test] + async fn test_serialization() { + let mut key_cache = KeyCache::new(); + let key_pair = Keypair::random(); + let uuid = Uuid::from_u128(1); + let password = PlainText::from(vec![1, 2, 3, 4, 5, 6]); + key_cache.add(key_pair, &uuid, password); + + key_cache.crypto.cipher.message = HexBytes::from(vec![7, 8, 9]); + key_cache.crypto.checksum.message = HexBytes::from(vec![10, 11, 12]); + + let binary = serde_json::to_vec(&key_cache).unwrap(); + let clone: KeyCache = serde_json::from_slice(binary.as_ref()).unwrap(); + + assert_eq!(clone.crypto, key_cache.crypto); + assert_eq!(clone.uuids, key_cache.uuids); + } + + #[tokio::test] + async fn test_encryption() { + let mut key_cache = KeyCache::new(); + let keypairs = vec![Keypair::random(), Keypair::random()]; + let uuids = vec![Uuid::from_u128(1), Uuid::from_u128(2)]; + let passwords = vec![ + PlainText::from(vec![1, 2, 3, 4, 5, 6]), + PlainText::from(vec![7, 8, 9, 10, 11, 12]), + ]; + + for ((keypair, uuid), password) in keypairs.iter().zip(uuids.iter()).zip(passwords.iter()) { + key_cache.add(keypair.clone(), uuid, password.clone()); + } + + key_cache.encrypt().unwrap(); + key_cache.state = State::DecryptedAndSaved; + + assert_eq!(&key_cache.uuids, &uuids); + + let mut new_clone = KeyCache { + crypto: key_cache.crypto.clone(), + uuids: key_cache.uuids.clone(), + pairs: Default::default(), + passwords: vec![], + state: State::NotDecrypted, + }; + + new_clone + .decrypt(passwords, keypairs.iter().map(|p| p.pk.clone()).collect()) + .unwrap(); + + let passwords_to_plain = |cache: &KeyCache| -> Vec<Vec<u8>> { + cache + .passwords + .iter() + .map(|x| x.as_bytes().to_vec()) + .collect() + }; + + assert_eq!(key_cache.crypto, new_clone.crypto); + assert_eq!( + passwords_to_plain(&key_cache), + passwords_to_plain(&new_clone) + ); + assert_eq!(key_cache.uuids, new_clone.uuids); + assert_eq!(key_cache.state, new_clone.state); + assert_eq!(key_cache.pairs.len(), new_clone.pairs.len()); + for (key, value) in key_cache.pairs { + assert!(new_clone.pairs.contains_key(&key)); + assert_eq!( + format!("{:?}", value), + format!("{:?}", new_clone.pairs[&key]) + ); + } + } +} diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 034271199..3b7dc6b07 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -6,6 +6,7 @@ mod duties_service; mod fork_service; mod initialized_validators; mod is_synced; +mod key_cache; mod notifier; mod validator_duty; mod validator_store; From a886afd3cad2cf4ed14752ccb697906916f6d2e6 Mon Sep 17 00:00:00 2001 From: Herman Junge <hermanjunge@protonmail.com> Date: Wed, 7 Oct 2020 00:31:19 +0000 Subject: [PATCH 26/32] Improve command help (#1740) A little help for the future generations. --- testing/simulator/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index 1ce8b2a5d..81ee5abb5 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -35,7 +35,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("speed_up_factor") .takes_value(true) .default_value("3") - .help("Speed up factor")) + .help("Speed up factor. Please use a divisor of 12.")) .arg(Arg::with_name("continue_after_checks") .short("c") .long("continue_after_checks") From a67fa5f4a4689029fde6de3184cf72203ecf50c8 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Wed, 7 Oct 2020 10:10:35 +0000 Subject: [PATCH 27/32] Add zinken testnet (#1741) ## Issue Addressed - Resolves #1722 ## Proposed Changes This extends @danielschonfeld's work in #1739 with: - Use an empty boot node list - Remove the genesis state ## Additional Info NA Co-authored-by: Daniel Schonfeld <daniel@schonfeld.org> --- common/eth2_config/src/lib.rs | 2 ++ common/eth2_testnet_config/build.rs | 3 ++- common/eth2_testnet_config/src/lib.rs | 7 +++++-- common/eth2_testnet_config/testnet_zinken.zip | Bin 0 -> 1485 bytes lighthouse/src/main.rs | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 common/eth2_testnet_config/testnet_zinken.zip diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 6ec5a2162..9696ac1d3 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -111,6 +111,8 @@ define_net!(medalla, include_medalla_file, "medalla", true); define_net!(spadina, include_spadina_file, "spadina", true); +define_net!(zinken, include_zinken_file, "zinken", false); + #[cfg(test)] mod tests { use super::*; diff --git a/common/eth2_testnet_config/build.rs b/common/eth2_testnet_config/build.rs index 588ec90a0..e9f4794d4 100644 --- a/common/eth2_testnet_config/build.rs +++ b/common/eth2_testnet_config/build.rs @@ -1,6 +1,6 @@ //! Downloads a testnet configuration from Github. -use eth2_config::{altona, medalla, spadina, Eth2NetArchiveAndDirectory}; +use eth2_config::{altona, medalla, spadina, zinken, Eth2NetArchiveAndDirectory}; use std::fs; use std::fs::File; use std::io; @@ -10,6 +10,7 @@ const ETH2_NET_DIRS: &[Eth2NetArchiveAndDirectory<'static>] = &[ altona::ETH2_NET_DIR, medalla::ETH2_NET_DIR, spadina::ETH2_NET_DIR, + zinken::ETH2_NET_DIR, ]; fn main() { diff --git a/common/eth2_testnet_config/src/lib.rs b/common/eth2_testnet_config/src/lib.rs index 1b0d4a933..37d532145 100644 --- a/common/eth2_testnet_config/src/lib.rs +++ b/common/eth2_testnet_config/src/lib.rs @@ -7,7 +7,9 @@ //! //! https://github.com/sigp/lighthouse/pull/605 //! -use eth2_config::{include_altona_file, include_medalla_file, include_spadina_file, unique_id}; +use eth2_config::{ + include_altona_file, include_medalla_file, include_spadina_file, include_zinken_file, unique_id, +}; use enr::{CombinedKey, Enr}; use ssz::{Decode, Encode}; @@ -54,8 +56,9 @@ macro_rules! define_net { const ALTONA: HardcodedNet = define_net!(altona, include_altona_file); const MEDALLA: HardcodedNet = define_net!(medalla, include_medalla_file); const SPADINA: HardcodedNet = define_net!(spadina, include_spadina_file); +const ZINKEN: HardcodedNet = define_net!(zinken, include_zinken_file); -const HARDCODED_NETS: &[HardcodedNet] = &[ALTONA, MEDALLA, SPADINA]; +const HARDCODED_NETS: &[HardcodedNet] = &[ALTONA, MEDALLA, SPADINA, ZINKEN]; pub const DEFAULT_HARDCODED_TESTNET: &str = "medalla"; /// Specifies an Eth2 testnet. diff --git a/common/eth2_testnet_config/testnet_zinken.zip b/common/eth2_testnet_config/testnet_zinken.zip new file mode 100644 index 0000000000000000000000000000000000000000..f81975f66c7dbe43c6bc95f3e8b088370e1c5fbc GIT binary patch literal 1485 zcmWIWW@h1H00Ew6_du6R83oKhHVE@F$S@@3=a<B%<`wBxCg$dZhHx@4a|G7Lvj)}1 zmsW5yFtWS=Dg+bJv0MQ_lSF{}I2g{!xCi#H@O{V$lyzihVBiLtl$@WJmYI$)r)6$U zJOidVxgn?XZW{>f`K;Yj$EfPP>8Z5tIvq8Wm6N83es?<H$nV-Xr@5|n`jy+A`fV1Q zcW*xLwK^(hPg1zW+qyr$KY#rG{WyP7YW}`U*H;(t9rz{Ivps0{<#i`IlAkf<HSQJN zf5FVN|KgitOCFrbG@fyG%VoW#l0hw9?=QSs^)d9a<kO`Nr!uEcyt@4V%}jNN))|%i z^gD8Q?V9$W_nnCQ_Sgn5DcLxOM$v1VPjyP`2&qLmUCwbXQCc!v#N=$Io2Kg7yH*B* z(M;*D)7MpBH9q`z>tw$zK@E=l*>2bC4i;aMx|{X5CCn#@cV=$Ot{jW%q)6V4`!swO zOJCvpw{)}L6{a7V<th(m9l0Lmb+=k?P4cQH!?hb_-xL`Z{M`1=(~&1QCVubRb6j^E zW^{{e>bNU$&-}`VbZzBZ6Bx=2j@vmb6_wu+crEhPE77?rEsLeQ*RGao+bU-B#D4F> z&2h7@FApzy%2RgM_}s>hZ(A16PmK(&SnfJcR{l$bLs$sE*W^uBGM2Sd`ZwJZe{XaD zc$;6}jnY?NzH}evDCIruGiRnu*EWA~u01Rvt67gujJS}VtRIs4fXlye*}_%Mb>;?U z*ShNZe$1P*^y}UNUh_4_H8XgxJjzWtb4*5M_u^eT?yR$w_`9bsF^q51J^Ic4;tIDi z#TzMeCM0$|`^$CH&6xYZ-YdVRnjWY~XnSR|Kkc<;$KJgUwI1wvwEcojRkV_cMf<G| zE@LAr4;!_o0=YN$TfW{r;m}?6g@t{yHt3udotea<V<EZpffI-5cdHxQ8?v9g^A8I5 z%Db`A;L_o<5rxaHZZ=juRqJ)gZ@Y#jU%PYH?9ES_Ubm(c&)3|d^I1hRC`Iha8J&A- z9pVep7I*Y~QEXtjGtutNwFZ50wd324aVPF~?thZ~O1t8kgtg7wY3uhGFyDA<c`l}_ zLW;@6=c<@jxW^px-FJQzz7}IQ*Dd@jA;x}hS@|BNhdgr{AL^XH7~btTUt@BYhv&8_ z1yg7AdF*m^(-pjxzi{oU6DPLua2M@iSmA$t;-q}d%ZaT0LSb7z22A)5%1w}bc^#OW zysok(a)9#|1A_oCH>IQ&<m6YzC*|ZPXX}+zlz_A5G+>tc56qruS<~3W!otMR9GpKP zCO!n3=&U1TtPL~~ghe1G<`-v{!~^qbNl{{Q3Eb4BKvVx<nrcvCY3XL*nrvWZYGRRO zU}5HJV&<G=U|?wJY;NIVkYt%;>1J$}>|$yRw3v}eju}@WBLQ>(0|O%vFKGm^(25*Z zNRfl)Cy0@_N*iP&y?{odls!NLL1hnC191g7vVk*zg&Jn?15E`5KOR#t10C7aR}fRN bL;=ubP!wR8%*qA|EfydQ0R~<&3y22*kk}c` literal 0 HcmV?d00001 diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 376381c27..714209b9c 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -114,7 +114,7 @@ fn main() { .long("testnet") .value_name("testnet") .help("Name of network lighthouse will connect to") - .possible_values(&["medalla", "altona", "spadina"]) + .possible_values(&["medalla", "altona", "spadina", "zinken"]) .conflicts_with("testnet-dir") .takes_value(true) .global(true) From b69c63d486d2293bd274fe39f0a573cb4d4fc07a Mon Sep 17 00:00:00 2001 From: realbigsean <seananderson33@gmail.com> Date: Thu, 8 Oct 2020 21:01:32 +0000 Subject: [PATCH 28/32] Validator dir creation (#1746) ## Issue Addressed Resolves #1744 ## Proposed Changes - Add `directory::ensure_dir_exists` to the `ValidatorDefinition::open_or_create` method - As @pawanjay176 suggested, making the `--validator-dir` non-global so users are forced to include the flag after the `validator` subcommand. Current behavior seems to be ignoring the flag if it comes after something like `validator import` ## Additional Info N/A --- Cargo.lock | 1 + account_manager/src/validator/mod.rs | 1 - account_manager/src/wallet/mod.rs | 1 - common/account_utils/Cargo.toml | 1 + common/account_utils/src/validator_definitions.rs | 6 ++++++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 878ee6946..4a0679b29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,7 @@ dependencies = [ name = "account_utils" version = "0.1.0" dependencies = [ + "directory", "eth2_keystore", "eth2_wallet", "rand 0.7.3", diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 99d8da01b..042a35ccd 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -26,7 +26,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Defaults to ~/.lighthouse/{testnet}/validators", ) .takes_value(true) - .global(true) .conflicts_with("datadir"), ) .subcommand(create::cli_app()) diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index d745cbcd2..4ab957ecb 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -18,7 +18,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("WALLETS_DIRECTORY") .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{testnet}/wallets") .takes_value(true) - .global(true) .conflicts_with("datadir"), ) .subcommand(create::cli_app()) diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index 7b8b2822e..1b7580ded 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -19,3 +19,4 @@ types = { path = "../../consensus/types" } validator_dir = { path = "../validator_dir" } regex = "1.3.9" rpassword = "5.0.0" +directory = { path = "../directory" } diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index b69678628..11cfa1c14 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -4,6 +4,7 @@ //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. use crate::{create_with_600_perms, default_keystore_password_path, ZeroizeString}; +use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; use serde_derive::{Deserialize, Serialize}; @@ -35,6 +36,8 @@ pub enum Error { InvalidKeystorePubkey, /// The keystore was unable to be opened. UnableToOpenKeystore(eth2_keystore::Error), + /// The validator directory could not be created. + UnableToCreateValidatorDir(PathBuf), } /// Defines how the validator client should attempt to sign messages for this validator. @@ -108,6 +111,9 @@ pub struct ValidatorDefinitions(Vec<ValidatorDefinition>); impl ValidatorDefinitions { /// Open an existing file or create a new, empty one if it does not exist. pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> { + ensure_dir_exists(validators_dir.as_ref()).map_err(|_| { + Error::UnableToCreateValidatorDir(PathBuf::from(validators_dir.as_ref())) + })?; let config_path = validators_dir.as_ref().join(CONFIG_FILENAME); if !config_path.exists() { let this = Self::default(); From 414138f137bf8384c939cd6188ce0dcad32cc47b Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Fri, 9 Oct 2020 00:43:49 +0000 Subject: [PATCH 29/32] Update docs for v0.3.0 (#1742) ## Issue Addressed NA ## Proposed Changes - Remove Metamask deposits from the docs. - Restructure docs to be launchpad-centric. - Remove references to sigp/lighthouse-docker. - Add section about binaries. ## Additional Info Please provide any additional information. For example, future considerations or information useful for reviewers. --- book/src/SUMMARY.md | 6 +- book/src/become-a-validator-docker.md | 121 -------------- book/src/become-a-validator-source.md | 219 ------------------------- book/src/become-a-validator.md | 96 ----------- book/src/docker.md | 26 ++- book/src/installation-binaries.md | 38 +++++ book/src/installation-source.md | 82 +++++++++ book/src/installation.md | 73 +-------- book/src/testnet-validator.md | 162 ++++++++++++++++++ book/src/validator-import-launchpad.md | 26 ++- 10 files changed, 337 insertions(+), 512 deletions(-) delete mode 100644 book/src/become-a-validator-docker.md delete mode 100644 book/src/become-a-validator-source.md delete mode 100644 book/src/become-a-validator.md create mode 100644 book/src/installation-binaries.md create mode 100644 book/src/installation-source.md create mode 100644 book/src/testnet-validator.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1ecfa9213..cbd252a90 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -1,11 +1,11 @@ # Summary * [Introduction](./intro.md) -* [Become a Validator](./become-a-validator.md) - * [Using Docker](./become-a-validator-docker.md) - * [Building from Source](./become-a-validator-source.md) +* [Become a Testnet Validator](./testnet-validator.md) * [Installation](./installation.md) + * [Pre-Built Binaries](./installation-binaries.md) * [Docker](./docker.md) + * [Build from Source](./installation-source.md) * [Raspberry Pi 4](./pi.md) * [Cross-Compiling](./cross-compiling.md) * [Key Management](./key-management.md) diff --git a/book/src/become-a-validator-docker.md b/book/src/become-a-validator-docker.md deleted file mode 100644 index ce45996fd..000000000 --- a/book/src/become-a-validator-docker.md +++ /dev/null @@ -1,121 +0,0 @@ -# Become a Validator: Using Docker - -Sigma Prime maintains the -[sigp/lighthouse-docker](https://github.com/sigp/lighthouse-docker) repository -which provides an easy way to run Lighthouse without building the Lighthouse -binary yourself. - -> Note: when you're running the Docker Hub image you're relying upon a -> pre-built binary instead of building from source. If you want the highest -> assurance you're running the _real_ Lighthouse, -> [build the docker image yourself](./docker.md) instead. You'll need some -> experience with docker-compose to integrate your locally built docker image -> with the docker-compose environment. - -## 0. Install Docker Compose - - Docker Compose relies on Docker Engine for any meaningful work, so make sure you have Docker Engine installed either locally or remote, depending on your setup. - -- On desktop systems like [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/) and [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/install/), Docker Compose is included as part of those desktop installs, so the desktop install is all you need. - -- On Linux systems, you'll need to first [install the Docker for your OS](https://docs.docker.com/install/#server) and then [follow the instuctions here](https://docs.docker.com/compose/install/#install-compose-on-linux-systems). - -> For more on installing Compose, see [here](https://docs.docker.com/compose/install/). - - -## 1. Clone the repository - -Once you have Docker Compose installed, clone the -[sigp/lighthouse-docker](https://github.com/sigp/lighthouse-docker) repository: - -```bash - git clone https://github.com/sigp/lighthouse-docker - cd lighthouse-docker -``` - -## 2. Configure the Docker environment - -Then, create a file named `.env` with the following contents (these values are -documented -[here](https://github.com/sigp/lighthouse-docker/blob/master/default.env)): - -```bash -DEBUG_LEVEL=info -START_GETH=true -START_VALIDATOR=true -VALIDATOR_COUNT=1 -VOTING_ETH1_NODE=http://geth:8545 -``` - -> To specify a non-default testnet add `TESTNET=<testnet>` to the above file. <testnet> can be `altona` or `medalla`. - -_This `.env` file should live in the `lighthouse-docker` directory alongside the -`docker-compose.yml` file_. - -## 3. Start Lighthouse - -Start the docker-compose environment (you may need to prefix the below command with `sudo`): - -```bash - docker-compose up -``` - -Watch the output of this command for the `Decrypted validator keystore pubkey` -log, as it contains your `voting_pubkey` -- the primary identifier for your new -validator. This key is useful for finding your validator in block explorers. -Here's an example of the log: - -```bash -validator_client_1 | Jun 01 00:29:24.418 INFO Decrypted validator keystore voting_pubkey: 0x9986ade7a974d2fe2d0fc84a8c04153873337d533d43a83439cab8ec276410686dd69aa808605a7324f34e52497a3f41 -``` -This is one of the earlier logs outputted, so you may have to scroll up or perform a search in your terminal to find it. - -> Note: `docker-compose up` generates a new sub-directory -- to store your validator's deposit data, along with its voting and withdrawal keys -- in the `lighthouse-data/validators` directory. This sub-directory is identified by your validator's `voting_pubkey` (the same `voting_pubkey` you see in the logs). So this is another way you can find it. - -> Note: the docker-compose setup includes a fast-synced geth node. So you can -> expect the `beacon_node` to log some eth1-related errors whilst the geth node -> boots and becomes synced. This will only happen on the first start of the -> compose environment or if geth loses sync. - -> Note: If you are participating in the genesis of a network (the network has -> not launched yet) you will notice errors in the validator client. This is -> because the beacon node not expose its HTTP API until -> the genesis of the network is known (approx 2 days before the network -> launches). - -> Note: Docker exposes ports TCP 9000 and UDP 9000 by default. Although not -> strictly required, we recommend setting up port forwards to expose these -> ports publicly. For more information see the FAQ or the [Advanced Networking](advanced_networking.html) -> section - -To find an estimate for how long your beacon node will take to finish syncing, look for logs that look like this: - -```bash -beacon_node_1 | Mar 16 11:33:53.979 INFO Syncing -est_time: 47 mins, speed: 16.67 slots/sec, distance: 47296 slots (7 days 14 hrs), peers: 3, service: slot_notifier -``` - -You'll find the estimated time under `est_time`. In the example above, that's `47 mins`. - -If your beacon node hasn't finished syncing yet, you'll see some ERRO messages indicating that your node hasn't synced yet: - -```bash -validator_client_1 | Mar 16 11:34:36.086 ERRO Beacon node is not synced current_epoch: 6999, node_head_epoch: 5531, service: duties -``` - -It's safest to wait for your node to sync before moving on to the next step, otherwise your validator may activate before you're able to produce blocks and attestations (and you may be penalized as a result). - -However, since it generally takes somewhere between [4 and 8 hours](./faq.md) after depositing for a validator to become active, if your `est_time` is less than 4 hours, you _should_ be fine to just move on to the next step. After all, this is a testnet and you're only risking Goerli ETH! - -## Installation complete! - -In the [next step](become-a-validator.html#2-submit-your-deposit-to-goerli) you'll need to upload your validator's deposit data. This data is stored in a file called `eth1_deposit_data.rlp`. - -You'll find it in `lighthouse-docker/.lighthouse/validators/` -- in the sub-directory that corresponds to your validator's public key (`voting_pubkey`). - - -> For example, if you ran [step 1](become-a-validator-docker.html#1-clone-the-repository) in `/home/karlm/`, and your validator's `voting_pubkey` is `0x8592c7..`, then you'll find your `eth1_deposit_data.rlp` file in the following directory: -> ->`/home/karlm/lighthouse-docker/.lighthouse/validators/0x8592c7../` - -Once you've located `eth1_deposit_data.rlp`, you're ready to move on to [Become a Validator: Step 2](become-a-validator.html#2-submit-your-deposit-to-goerli). diff --git a/book/src/become-a-validator-source.md b/book/src/become-a-validator-source.md deleted file mode 100644 index b1a8db045..000000000 --- a/book/src/become-a-validator-source.md +++ /dev/null @@ -1,219 +0,0 @@ -# Become a Validator: Building from Source - -## 0. Install Rust -If you don't have Rust installed already, visit [rustup.rs](https://rustup.rs/) to install it. - -> Notes: -> - If you're not familiar with Rust or you'd like more detailed instructions, see our [installation guide](./installation.md). -> - Windows is presently only supported via [WSL](https://docs.microsoft.com/en-us/windows/wsl/about). - - -## 1. Download and install Lighthouse - -Once you have Rust installed, you can install Lighthouse with the following commands: - -1. `git clone https://github.com/sigp/lighthouse.git` -2. `cd lighthouse` -4. `make` - -You may need to open a new terminal window before running `make`. - -You've completed this step when you can run `$ lighthouse --help` and see the -help menu. - - -## 2. Start an Eth1 client - -Since Eth2 relies upon the Eth1 chain for validator on-boarding, all Eth2 validators must have a connection to an Eth1 node. - -We provide instructions for using Geth (the Eth1 client that, by chance, we ended up testing with), but you could use any client that implements the JSON RPC via HTTP. A fast-synced node should be sufficient. - -### Installing Geth -If you're using a Mac, follow the instructions [listed here](https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Mac) to install geth. Otherwise [see here](https://github.com/ethereum/go-ethereum/wiki/Installing-Geth). - -### Starting Geth - -Once you have geth installed, use this command to start your Eth1 node: - -```bash - geth --goerli --http -``` - -## 3. Start your beacon node - -The beacon node is the core component of Eth2, it connects to other peers over -the internet and maintains a view of the chain. - -Start your beacon node with: - -```bash - lighthouse --testnet medalla beacon --staking -``` - -> The `--testnet` parameter is optional. Omitting it will default to the -> current public testnet. Set the value to the testnet you wish to run on. -> Current values are either `altona` or `medalla`. This is true for all the -> following commands in this document. - -> Note: Lighthouse, by default, opens port 9000 over TCP and UDP. Although not -> strictly required, we recommend setting up port forwards to expose these -> ports publicly. For more information see the FAQ or the [Advanced Networking](advanced_networking.html) -> section - - -You can also pass an external http endpoint (e.g. Infura) for the Eth1 node using the `--eth1-endpoint` flag: - -```bash - lighthouse --testnet medalla beacon --staking --eth1-endpoint <ETH1-SERVER> -``` - -Your beacon node has started syncing when you see the following (truncated) -log: - -``` -Dec 09 12:57:18.026 INFO Syncing -est_time: 2 hrs ... -``` - -The `distance` value reports the time since eth2 genesis, whilst the `est_time` -reports an estimate of how long it will take your node to become synced. - -You'll know it's finished syncing once you see the following (truncated) log: - -``` -Dec 09 12:27:06.010 INFO Synced -slot: 16835, ... -``` - - -## 4. Generate your validator key - -First, [create a wallet](./wallet-create.md) that can be used to generate -validator keys. Then, from that wallet [create a -validator](./validator-create.md). A two-step example follows: - -### 4.1 Create a Wallet - -Create a wallet with: - -```bash -lighthouse --testnet medalla account wallet create -``` - -You will be prompted for a wallet name and a password. The output will look like this: - -``` -Your wallet's 24-word BIP-39 mnemonic is: - - glad marble art pelican nurse large guilt response brave affair kite essence welcome gauge peace once picnic debris devote ticket blood bike solar junk - -This mnemonic can be used to fully restore your wallet, should -you lose the JSON file or your password. - -It is very important that you DO NOT SHARE this mnemonic as it will -reveal the private keys of all validators and keys generated with -this wallet. That would be catastrophic. - -It is also important to store a backup of this mnemonic so you can -recover your private keys in the case of data loss. Writing it on -a piece of paper and storing it in a safe place would be prudent. - -Your wallet's UUID is: - - 1c8c13d5-d065-4ef7-bad3-14e9d8146140 - -You do not need to backup your UUID or keep it secret. -``` - -**Don't forget to make a backup** of the 24-word BIP-39 mnemonic. It can be -used to restore your validator if there is a data loss. - -### 4.2 Create a Validator from the Wallet - -Create a validator from the wallet with: - -```bash -lighthouse --testnet medalla account validator create --count 1 -``` - -Enter your wallet's name and password when prompted. The output will look like this: - -```bash -1/1 0x80f3dce8d6745a725d8442c9bc3ca0852e772394b898c95c134b94979ebb0af6f898d5c5f65b71be6889185c486918a7 -``` - -Take note of the _validator public key_ (the `0x` and 64 characters following -it). It's the validator's primary identifier, and will be used to find your -validator in block explorers. (The `1/1` at the start is saying it's one-of-one -keys generated). - -Once you've observed the validator public key, you've successfully generated a -new sub-directory for your validator in the `.lighthouse/validators` directory. -The sub-directory is identified by your validator's public key . And is used to -store your validator's deposit data, along with its voting keys and other -information. - - -## 5. Start your validator client - -> Note: If you are participating in the genesis of a network (the network has -> not launched yet) you should skip this step and re-run this step two days before -> the launch of the network. The beacon node does not expose its HTTP API until -> the genesis of the network is known (approx 2 days before the network -> launches). - -Since the validator client stores private keys and signs messages generated by the beacon node, for security reasons it runs separately from it. - -You'll need both your beacon node _and_ validator client running if you want to -stake. - -Start the validator client with: - -```bash - lighthouse --testnet medalla validator --auto-register -``` - -The `--auto-register` flag registers your signing key with the slashing protection database, which -keeps track of all the messages your validator signs. This flag should be used sparingly, -as reusing the same key on multiple nodes can lead to your validator getting slashed. On subsequent -runs you should leave off the `--auto-register` flag. - -You know that your validator client is running and has found your validator keys from [step 3](become-a-validator-source.html#3-start-your-beacon-node) when you see the following logs: - -``` -Dec 09 13:08:59.171 INFO Loaded validator keypair store voting_validators: 1 -Dec 09 13:09:09.000 INFO Awaiting activation slot: 17787, ... -``` - - -To find an estimate for how long your beacon node will take to finish syncing, lookout for the following logs: - -```bash -beacon_node_1 | Mar 16 11:33:53.979 INFO Syncing -est_time: 47 mins, speed: 16.67 slots/sec, distance: 47296 slots (7 days 14 hrs), peers: 3, service: slot_notifier -``` - -You'll find the estimated time under `est_time`. In the example log above, that's `47 mins`. - -If your beacon node hasn't finished syncing yet, you'll see some `ERRO` -messages indicating that your node hasn't synced yet: - -```bash -validator_client_1 | Mar 16 11:34:36.086 ERRO Beacon node is not synced current_epoch: 6999, node_head_epoch: 5531, service: duties -``` - -It's safest to wait for your node to sync before moving on to the next step, otherwise your validator may activate before you're able to produce blocks and attestations (and you may be penalized as a result). - -However, since it generally takes somewhere between [4 and 8 hours](./faq.md) after depositing for a validator to become active, if your `est_time` is less than 4 hours, you _should_ be fine to just move on to the next step. After all, this is a testnet and you're only risking Goerli ETH! - -## Installation complete! - -In the [next step](become-a-validator.html#2-submit-your-deposit-to-goerli) you'll need to upload your validator's deposit data. This data is stored in a file called `eth1_deposit_data.rlp`. - -You'll find it in `/home/.lighthouse/validators` -- in the sub-directory that corresponds to your validator's public key. - -> For example, if your username is `karlm`, and your validator's public key (aka `voting_pubkey`) is `0x8592c7..`, then you'll find your `eth1_deposit_data.rlp` file in the following directory: -> ->`/home/karlm/.lighthouse/validators/0x8592c7../` - -Once you've located your `eth1_deposit_data.rlp` file, you're ready to move on to [Become a Validator: Step 2](become-a-validator.html#2-submit-your-deposit-to-goerli). diff --git a/book/src/become-a-validator.md b/book/src/become-a-validator.md deleted file mode 100644 index a8f01c38b..000000000 --- a/book/src/become-a-validator.md +++ /dev/null @@ -1,96 +0,0 @@ -# Become an Ethereum 2.0 Validator - -There are two public testnets currently available. [Medalla](https://github.com/goerli/medalla/tree/master/medalla) and [Altona](https://github.com/goerli/medalla/tree/master/altona). Lighthouse supports both out of the box and joining these multi-client testnets is easy if you're familiar with the terminal. - -Lighthouse runs on Linux, MacOS and Windows and has a Docker work-flow to make -things as simple as possible. - -## 0. Acquire Goerli ETH -Before you install Lighthouse, you'll need [Metamask](https://metamask.io/) and 32 gETH -(Goerli ETH). We recommend the [mudit.blog -faucet](https://faucet.goerli.mudit.blog/) for those familiar with Goerli, or -[goerli.net](https://goerli.net/) for an overview of the testnet. - -> If this is your first time using Metamask and/or interacting with an Ethereum test network, we recommend going through the beginning of [this guide](https://hack.aragon.org/docs/guides-use-metamask) first (up to the *Signing your first transaction with MetaMask* section). - -## 1. Install and start Lighthouse - -There are two, different ways to install and start a Lighthouse validator: - -1. [Using `docker-compose`](./become-a-validator-docker.md): this is the easiest method. - -2. [Building from source](./become-a-validator-source.md): this is a little more involved, however it - gives a more hands-on experience. - -Once you've completed **either one** of these steps, you can move onto the next step. - -> Take note when running Lighthouse. Use the --testnet parameter to specify the testnet you whish to participate in. Medalla is currently the default, so make sure to use --testnet altona to join the Altona testnet. - -## 2. Submit your deposit to Goerli - -<div class="form-signin" id="uploadDiv"> - <p>Upload the <code>eth1_deposit_data.rlp</code> file from your validator - directory (created in the previous step) to submit your 32 Goerli-ETH - deposit using Metamask.</p> - <p>Note that the method you used in step 1 will determine where this file is - located.</p> - <input id="fileInput" type="file" style="display: none"> - <button id="uploadButton" class="btn btn-lg btn-primary btn-block" - type="submit">Upload and Submit Deposit</button> -</div> - -<div class="form-signin" id="waitingDiv" style="display: none"> - <p style="color: green">Your validator deposit was submitted and this step is complete.</p> - <p>See the transaction on <a id="txLink" target="_blank" - href="https://etherscan.io">Etherscan</a> - or <a href="">reload</a> to perform another deposit.</p> -</div> - -<div class="form-signin" id="errorDiv" style="display: none"> - <h4 class="h3 mb-3 font-weight-normal">Error</h4> - <p id="errorText" style="color: red">Unknown error.</p> - <p style="color: red">Please refresh to reupload.</p> -</div> - -> This deposit is made using gETH (Goerli ETH) which has no real value. Please don't ever -> send _real_ ETH to our deposit contract! - -## 3. Leave Lighthouse running - -Leave your beacon node and validator client running and you'll see logs as the -beacon node stays synced with the network while the validator client produces -blocks and attestations. - -It will take 4-8+ hours for the beacon chain to process and activate your -validator, however you'll know you're active when the validator client starts -successfully publishing attestations each slot: - -``` -Dec 03 08:49:40.053 INFO Successfully published attestation slot: 98, committee_index: 0, head_block: 0xa208…7fd5, -``` - -Although you'll produce an attestation each slot, it's less common to produce a -block. Watch for the block production logs too: - -``` -Dec 03 08:49:36.225 INFO Successfully published block slot: 98, attestations: 2, deposits: 0, service: block -``` - -If you see any `ERRO` (error) logs, please reach out on -[Discord](https://discord.gg/cyAszAh) or [create an -issue](https://github.com/sigp/lighthouse/issues/new). - -Don't forget to checkout the open-source block explorer for the Lighthouse -testnet at -[lighthouse-testnet3.beaconcha.in](https://lighthouse-testnet3.beaconcha.in/). - -Happy staking! - - -<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> -<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> -<script charset="utf-8" - src="https://cdn.ethers.io/scripts/ethers-v4.min.js" - type="text/javascript"> -</script> -<script src="js/deposit.js"></script> diff --git a/book/src/docker.md b/book/src/docker.md index 8a09e2bb4..ad6e2f1a1 100644 --- a/book/src/docker.md +++ b/book/src/docker.md @@ -1,11 +1,7 @@ # Docker Guide This repository has a `Dockerfile` in the root which builds an image with the -`lighthouse` binary installed. - -A pre-built image is available on Docker Hub and the -[sigp/lighthouse](https://github.com/sigp/lighthouse-docker) repository -contains a full-featured `docker-compose` environment. +`lighthouse` binary installed. A pre-built image is available on Docker Hub. ## Obtaining the Docker image @@ -20,10 +16,28 @@ Lighthouse maintains the Docker Hub repository which provides an easy way to run Lighthouse without building the image yourself. +Obtain the latest image with: + +```bash +$ docker pull sigp/lighthouse +``` + Download and test the image with: ```bash -$ docker run sigp/lighthouse lighthouse --help +$ docker run sigp/lighthouse lighthouse --version +``` + +If you can see the latest [Lighthouse +release](https://github.com/sigp/lighthouse/releases) version (see example +below), then you've +successfully installed Lighthouse via Docker. + +#### Example Version Output + +``` +Lighthouse vx.x.xx-xxxxxxxxx +BLS Library: xxxx-xxxxxxx ``` > Note: when you're running the Docker Hub image you're relying upon a diff --git a/book/src/installation-binaries.md b/book/src/installation-binaries.md new file mode 100644 index 000000000..2c01f9496 --- /dev/null +++ b/book/src/installation-binaries.md @@ -0,0 +1,38 @@ +# Pre-built Binaries + +Each Lighthouse release contains several downloadable binaries in the "Assets" +section of the release. You can find the [releases +on Github](https://github.com/sigp/lighthouse/releases). + +> Note: binaries are not yet provided for MacOS or Windows native. + +## Platforms + +Binaries are supplied for two platforms: + +- `x86_64-unknown-linux-gnu`: AMD/Intel 64-bit processors (most desktops, laptops, servers) +- `aarch64-unknown-linux-gnu`: 64-bit ARM processors (Raspberry Pi 4) + +Additionally there is also a `-portable` suffix which indicates if the `portable` feature is used: + +- Without `portable`: uses modern CPU instructions to provide the fastest signature verification times (may cause `Illegal instruction` error on older CPUs) +- With `portable`: approx. 20% slower, but should work on all modern 64-bit processors. + +## Usage + +Each binary is contained in a `.tar.gz` archive. For this example, lets use the +`v0.2.13` release and assume the user needs a portable `x86_64` binary. + +> Whilst this example uses `v0.2.13` we recommend always using the latest release. + +### Steps + +1. Go to the [Releases](https://github.com/sigp/lighthouse/releases) page and + select the latest release. +1. Download the `lighthouse-${VERSION}-x86_64-unknown-linux-gnu-portable.tar.gz` binary. +1. Extract the archive: + 1. `cd Downloads` + 1. `tar -xvf lighthouse-${VERSION}-x86_64-unknown-linux-gnu.tar.gz` +1. Test the binary with `./lighthouse --version` (it should print the version). +1. (Optional) Move the `lighthouse` binary to a location in your `PATH`, so the `lighthouse` command can be called from anywhere. + - E.g., `cp lighthouse /usr/bin` diff --git a/book/src/installation-source.md b/book/src/installation-source.md new file mode 100644 index 000000000..1f05c3e8d --- /dev/null +++ b/book/src/installation-source.md @@ -0,0 +1,82 @@ +# Installation: Build from Source + +Lighthouse builds on Linux, macOS, and Windows (via [WSL][] only). + +Compilation should be easy. In fact, if you already have Rust installed all you +need is: + +- `git clone https://github.com/sigp/lighthouse.git` +- `cd lighthouse` +- `make` + +If this doesn't work or is not clear enough, see the [Detailed +Instructions](#detailed-instructions) below. If you have further issues, see +[Troubleshooting](#troubleshooting). If you'd prefer to use Docker, see the +[Docker Guide](./docker.md). + +## Detailed Instructions + +1. Install Rust and Cargo with [rustup](https://rustup.rs/). + - Use the `stable` toolchain (it's the default). + - Check the [Troubleshooting](#troubleshooting) section for additional + dependencies (e.g., `cmake`). +1. Clone the Lighthouse repository. + - Run `$ git clone https://github.com/sigp/lighthouse.git` + - Change into the newly created directory with `$ cd lighthouse` +1. Build Lighthouse with `$ make`. +1. Installation was successful if `$ lighthouse --help` displays the + command-line documentation. + +> First time compilation may take several minutes. If you experience any +> failures, please reach out on [discord](https://discord.gg/cyAszAh) or +> [create an issue](https://github.com/sigp/lighthouse/issues/new). + +## Windows Support + +Compiling or running Lighthouse natively on Windows is not currently supported. However, +Lighthouse can run successfully under the [Windows Subsystem for Linux (WSL)][WSL]. If using +Ubuntu under WSL, you can should install the Ubuntu dependencies listed in the [Dependencies +(Ubuntu)](#dependencies-ubuntu) section. + +[WSL]: https://docs.microsoft.com/en-us/windows/wsl/about + +## Troubleshooting + +### Dependencies + +#### Ubuntu + +Several dependencies may be required to compile Lighthouse. The following +packages may be required in addition a base Ubuntu Server installation: + +```bash +sudo apt install -y git gcc g++ make cmake pkg-config libssl-dev +``` + +#### macOS + +You will need `cmake`. You can install via homebrew: + + brew install openssl cmake + +### Command is not found + +Lighthouse will be installed to `CARGO_HOME` or `$HOME/.cargo`. This directory +needs to be on your `PATH` before you can run `$ lighthouse`. + +See ["Configuring the `PATH` environment variable" +(rust-lang.org)](https://www.rust-lang.org/tools/install) for more information. + +### Compilation error + +Make sure you are running the latest version of Rust. If you have installed Rust using rustup, simply type `$ rustup update`. + +### OpenSSL + +If you get a build failure relating to OpenSSL, try installing `openssl-dev` or +`libssl-dev` using your OS package manager. + +- Ubuntu: `$ apt-get install libssl-dev`. +- Amazon Linux: `$ yum install openssl-devel`. + +[WSL]: https://docs.microsoft.com/en-us/windows/wsl/about diff --git a/book/src/installation.md b/book/src/installation.md index 94c6bcdd3..25aa8040c 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -1,73 +1,16 @@ # 📦 Installation -Lighthouse runs on Linux, macOS, and Windows via [WSL][]. -Installation should be easy. In fact, if you already have Rust installed all you need is: +Lighthouse runs on Linux, macOS, and Windows (via [WSL][] only). -- `git clone https://github.com/sigp/lighthouse.git` -- `cd lighthouse` -- `make` +There are three core methods to obtain the Lighthouse application: -If this doesn't work or is not clear enough, see the [Detailed Instructions](#detailed-instructions). If you have further issues, see [Troubleshooting](#troubleshooting). If you'd prefer to use Docker, see the [Docker Guide](./docker.md). +- [Pre-built binaries](./installation-binaries.md). +- [Docker images](./docker.md). +- [Building from source](./installation-source.md). -## Detailed Instructions +Additionally, there are two extra guides for specific uses: -1. Install Rust and Cargo with [rustup](https://rustup.rs/). - - Use the `stable` toolchain (it's the default). -1. Clone the Lighthouse repository. - - Run `$ git clone https://github.com/sigp/lighthouse.git` - - Change into the newly created directory with `$ cd lighthouse` -1. Build Lighthouse with `$ make`. -1. Installation was successful if `$ lighthouse --help` displays the - command-line documentation. - -> First time compilation may take several minutes. If you experience any -> failures, please reach out on [discord](https://discord.gg/cyAszAh) or -> [create an issue](https://github.com/sigp/lighthouse/issues/new). - -## Windows Support - -Compiling or running Lighthouse natively on Windows is not currently supported. However, -Lighthouse can run successfully under the [Windows Subsystem for Linux (WSL)][WSL]. If using -Ubuntu under WSL, you can should install the Ubuntu dependencies listed in the [Dependencies -(Ubuntu)](#dependencies-ubuntu) section. - -## Troubleshooting - -### Dependencies - -#### Ubuntu - -Several dependencies may be required to compile Lighthouse. The following -packages may be required in addition a base Ubuntu Server installation: - -```bash -sudo apt install -y git gcc g++ make cmake pkg-config libssl-dev -``` - -#### macOS - -You will need `cmake`. You can install via homebrew: - - brew install cmake - -### Command is not found - -Lighthouse will be installed to `CARGO_HOME` or `$HOME/.cargo`. This directory -needs to be on your `PATH` before you can run `$ lighthouse`. - -See ["Configuring the `PATH` environment variable" -(rust-lang.org)](https://www.rust-lang.org/tools/install) for more information. - -### Compilation error - -Make sure you are running the latest version of Rust. If you have installed Rust using rustup, simply type `$ rustup update`. - -### OpenSSL - -If you get a build failure relating to OpenSSL, try installing `openssl-dev` or -`libssl-dev` using your OS package manager. - -- Ubuntu: `$ apt-get install libssl-dev`. -- Amazon Linux: `$ yum install openssl-devel`. +- [Rapsberry Pi 4 guide](./pi.md). +- [Cross-compiling guide for developers](./cross-compiling.md). [WSL]: https://docs.microsoft.com/en-us/windows/wsl/about diff --git a/book/src/testnet-validator.md b/book/src/testnet-validator.md new file mode 100644 index 000000000..59b05f881 --- /dev/null +++ b/book/src/testnet-validator.md @@ -0,0 +1,162 @@ +# Become a Testnet Validator + +Joining an Eth2 testnet is a great way to get familiar with staking in Phase 0. +All users should experiment with a testnet prior to staking mainnet ETH. + +## Supported Testnets + +Lighthouse supports four testnets: + +- [Medalla](https://github.com/goerli/medalla/tree/master/medalla) (default) +- [Zinken](https://github.com/goerli/medalla/tree/master/zinken) +- [Spadina](https://github.com/goerli/medalla/tree/master/spadina) (deprecated) +- [Altona](https://github.com/goerli/medalla/tree/master/altona) (deprecated) + +When using Lighthouse, the `--testnet` flag selects a testnet. E.g., + +- `lighthouse` (no flag): Medalla. +- `lighthouse --testnet medalla`: Medalla. +- `lighthouse --testnet zinken`: Zinken. + +Using the correct `--testnet` flag is very important; using the wrong flag can +result in penalties, slashings or lost deposits. As a rule of thumb, always +provide a `--testnet` flag instead of relying on the default. + +> Note: In these documents we use `--testnet MY_TESTNET` for demonstration. You +> must replace `MY_TESTNET` with a valid testnet name. + +## Joining a Testnet + +There are five primary steps to become a testnet validator: + +1. Create validator keys and submit deposits. +1. Start an Eth1 client. +1. Install Lighthouse. +1. Import the validator keys into Lighthouse. +1. Start Lighthouse. +1. Leave Lighthouse running. + +Each of these primary steps has several intermediate steps, so we recommend +setting aside one or two hours for this process. + +### Step 1. Create validator keys + +The Ethereum Foundation provides an "Eth2 launch pad" for each active testnet: + +- [Medalla launchpad](https://medalla.launchpad.ethereum.org/) +- [Zinken launchpad](https://zinken.launchpad.ethereum.org/) + +Please follow the steps on the appropriate launch pad site to generate +validator keys and submit deposits. Make sure you select "Lighthouse" as your +client. + +Move to the next step once you have completed the steps on the launch pad, +including generating keys via the Python CLI and submitting gETH/ETH deposits. + +### Step 2. Start an Eth1 client + +Since Eth2 relies upon the Eth1 chain for validator on-boarding, all Eth2 validators must have a connection to an Eth1 node. + +We provide instructions for using Geth (the Eth1 client that, by chance, we ended up testing with), but you could use any client that implements the JSON RPC via HTTP. A fast-synced node should be sufficient. + +#### Installing Geth + +If you're using a Mac, follow the instructions [listed here](https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Mac) to install geth. Otherwise [see here](https://github.com/ethereum/go-ethereum/wiki/Installing-Geth). + +#### Starting Geth + +Once you have geth installed, use this command to start your Eth1 node: + +```bash + geth --goerli --http +``` + +### Step 3. Install Lighthouse + +*Note: Lighthouse only supports Windows via WSL.* + +Follow the [Lighthouse Installation Instructions](./installation.md) to install +Lighthouse from one of the available options. + +Proceed to the next step once you've successfully installed Lighthouse and view +its `--version` info. + +> Note: Some of the instructions vary when using Docker, ensure you follow the +> appropriate sections later in this guide. + +### Step 4. Import validator keys to Lighthouse + +When Lighthouse is installed, follow the [Importing from the Ethereum 2.0 Launch +pad](./validator-import-launchpad.md) instructions so the validator client can +perform your validator duties. + +Proceed to the next step once you've successfully imported all validators. + +### Step 5. Start Lighthouse + +For staking, one needs to run two Lighthouse processes: + +- `lighthouse bn`: the "beacon node" which connects to the P2P network and + verifies blocks. +- `lighthouse vc`: the "validator client" which manages validators, using data + obtained from the beacon node via a HTTP API. + +Starting these processes is different for binary and docker users: + +#### Binary users + +Those using the pre- or custom-built binaries can start the two processes with: + +```bash +lighthouse --testnet MY_TESTNET bn --staking +``` + +```bash +lighthouse --testnet MY_TESTNET vc +``` + +#### Docker users + +Those using Docker images can start the processes with: + +```bash +$ docker run \ + --network host \ + -v $HOME/.lighthouse:/root/.lighthouse sigp/lighthouse \ + lighthouse --testnet MY_TESTNET bn --staking --http-address 0.0.0.0 +``` + +```bash +$ docker run \ + --network host \ + -v $HOME/.lighthouse:/root/.lighthouse \ + sigp/lighthouse \ + lighthouse --testnet MY_TESTNET vc +``` + +### Step 6. Leave Lighthouse running + +Leave your beacon node and validator client running and you'll see logs as the +beacon node stays synced with the network while the validator client produces +blocks and attestations. + +It will take 4-8+ hours for the beacon chain to process and activate your +validator, however you'll know you're active when the validator client starts +successfully publishing attestations each epoch: + +``` +Dec 03 08:49:40.053 INFO Successfully published attestation slot: 98, committee_index: 0, head_block: 0xa208…7fd5, +``` + +Although you'll produce an attestation each epoch, it's less common to produce a +block. Watch for the block production logs too: + +``` +Dec 03 08:49:36.225 INFO Successfully published block slot: 98, attestations: 2, deposits: 0, service: block +``` + +If you see any `ERRO` (error) logs, please reach out on +[Discord](https://discord.gg/cyAszAh) or [create an +issue](https://github.com/sigp/lighthouse/issues/new). + +Happy staking! diff --git a/book/src/validator-import-launchpad.md b/book/src/validator-import-launchpad.md index 3688b139c..b9d444b86 100644 --- a/book/src/validator-import-launchpad.md +++ b/book/src/validator-import-launchpad.md @@ -1,4 +1,4 @@ -# Importing from the Ethereum 2.0 Launchpad +# Importing from the Ethereum 2.0 Launch pad The [Eth2 Lauchpad](https://github.com/ethereum/eth2.0-deposit) is a website from the Ethereum Foundation which guides users how to use the @@ -20,7 +20,7 @@ Whilst following the steps on the website, users are instructed to download the repository. This `eth2-deposit-cli` script will generate the validator BLS keys into a `validator_keys` directory. We assume that the user's present-working-directory is the `eth2-deposit-cli` repository (this is where -you will be if you just ran the `./deposit.sh` script from the Eth2 Launchpad +you will be if you just ran the `./deposit.sh` script from the Eth2 Launch pad website). If this is not the case, simply change the `--directory` to point to the `validator_keys` directory. @@ -30,6 +30,9 @@ using the standard `validators` directory (specify a different one using ### 1. Run the `lighthouse account validator import` command. +Docker users should use the command from the [Docker](#docker) +section, all other users can use: + ```bash lighthouse account validator import --directory validator_keys @@ -85,3 +88,22 @@ INFO Enabled validator voting_pubkey: 0xa5e8702533f6d66422e042a0bf3471ab9b Once this log appears (and there are no errors) the `lighthouse vc` application will ensure that the validator starts performing its duties and being rewarded by the protocol. There is no more input required from the user. + +## Docker + +The `import` command is a little more complex for Docker users, but the example +in this document can be substituted with: + +```bash +docker run -it \ + -v $HOME/.lighthouse:/root/.lighthouse \ + -v $(pwd)/validator_keys:/root/validator_keys \ + sigp/lighthouse \ + lighthouse --testnet medalla account validator import --directory /root/validator_keys +``` + +Here we use two `-v` volumes to attach: + +- `~/.lighthouse` on the host to `/root/.lighthouse` in the Docker container. +- The `validator_keys` directory in the present working directory of the host + to the `/root/validator_keys` directory of the Docker container. From 72cc5e35afb7b3477060b2932abff764d46a3543 Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Fri, 9 Oct 2020 02:05:30 +0000 Subject: [PATCH 30/32] Bump version to v0.3.0 (#1743) ## Issue Addressed NA ## Proposed Changes - Bump version to v0.3.0 - Run `cargo update` ## Additional Info NA --- Cargo.lock | 267 +++++++++--------- account_manager/Cargo.toml | 2 +- beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/eth2_config/src/lib.rs | 2 +- common/eth2_testnet_config/testnet_zinken.zip | Bin 1485 -> 319297 bytes common/lighthouse_version/src/lib.rs | 2 +- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- validator_client/Cargo.toml | 2 +- 10 files changed, 146 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a0679b29..17184a10f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,7 +2,7 @@ # It is not intended for manual editing. [[package]] name = "account_manager" -version = "0.2.13" +version = "0.3.0" dependencies = [ "account_utils", "bls", @@ -18,7 +18,7 @@ dependencies = [ "eth2_testnet_config", "eth2_wallet", "eth2_wallet_manager", - "futures 0.3.5", + "futures 0.3.6", "hex 0.4.2", "libc", "rand 0.7.3", @@ -202,6 +202,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +[[package]] +name = "ahash" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adac150c2dd5a9c864d054e07bda5e6bc010cd10036ea5f17e82a2f5867f735" + [[package]] name = "aho-corasick" version = "0.7.13" @@ -231,9 +237,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" +checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" [[package]] name = "arbitrary" @@ -352,7 +358,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.4.2", + "miniz_oxide 0.4.3", "object", "rustc-demangle", ] @@ -402,7 +408,7 @@ dependencies = [ "eth2_ssz_types", "exit-future", "fork_choice", - "futures 0.3.5", + "futures 0.3.6", "genesis", "int_to_bytes", "integer-sqrt", @@ -442,7 +448,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "0.2.13" +version = "0.3.0" dependencies = [ "beacon_chain", "clap", @@ -457,7 +463,7 @@ dependencies = [ "eth2_ssz", "eth2_testnet_config", "exit-future", - "futures 0.3.5", + "futures 0.3.6", "genesis", "hex 0.4.2", "hyper 0.13.8", @@ -514,7 +520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11593270830d9b037fbead730bb0c05ef6fbf6be55537a1e8e5892edef7e1f03" dependencies = [ "funty", - "radium 0.5.1", + "radium 0.5.3", "tap", "wyz", ] @@ -640,14 +646,14 @@ dependencies = [ [[package]] name = "boot_node" -version = "0.2.13" +version = "0.3.0" dependencies = [ "beacon_node", "clap", "eth2_libp2p", "eth2_ssz", "eth2_testnet_config", - "futures 0.3.5", + "futures 0.3.6", "hex 0.4.2", "log 0.4.11", "logging", @@ -882,7 +888,7 @@ dependencies = [ "eth2_config", "eth2_libp2p", "eth2_ssz", - "futures 0.3.5", + "futures 0.3.6", "genesis", "http_api", "http_metrics", @@ -1369,7 +1375,7 @@ dependencies = [ "digest 0.8.1", "enr", "fnv", - "futures 0.3.5", + "futures 0.3.6", "hex 0.4.2", "hkdf", "lazy_static", @@ -1496,7 +1502,7 @@ dependencies = [ "eth2_config", "eth2_testnet_config", "exit-future", - "futures 0.3.5", + "futures 0.3.6", "logging", "parking_lot 0.11.0", "slog", @@ -1528,7 +1534,7 @@ dependencies = [ "eth2_hashing", "eth2_ssz", "eth2_ssz_derive", - "futures 0.3.5", + "futures 0.3.6", "hex 0.4.2", "lazy_static", "libflate", @@ -1554,7 +1560,7 @@ name = "eth1_test_rig" version = "0.2.0" dependencies = [ "deposit_contract", - "futures 0.3.5", + "futures 0.3.6", "serde_json", "tokio 0.2.22", "types", @@ -1667,7 +1673,7 @@ dependencies = [ "eth2_ssz_types", "exit-future", "fnv", - "futures 0.3.5", + "futures 0.3.6", "hashset_delay", "hex 0.4.2", "lazy_static", @@ -1814,7 +1820,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e43f2f1833d64e33f15592464d6fdd70f349dda7b1a53088eb83cd94014008c5" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", ] [[package]] @@ -1956,15 +1962,15 @@ checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8" [[package]] name = "futures" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" +checksum = "4c7e4c2612746b0df8fed4ce0c69156021b704c9aefa360311c04e6e9e002eed" [[package]] name = "futures" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +checksum = "5d8e3078b7b2a8a671cb7a3d17b4760e4181ea243227776ba83fd043b4ca034e" dependencies = [ "futures-channel", "futures-core", @@ -1977,9 +1983,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +checksum = "a7a4d35f7401e948629c9c3d6638fb9bf94e0b2121e96c3b428cc4e631f3eb74" dependencies = [ "futures-core", "futures-sink", @@ -1987,9 +1993,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" +checksum = "d674eaa0056896d5ada519900dbf97ead2e46a7b6621e8160d79e2f2e1e2784b" [[package]] name = "futures-cpupool" @@ -1997,15 +2003,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "num_cpus", ] [[package]] name = "futures-executor" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +checksum = "cc709ca1da6f66143b8c9bec8e6260181869893714e9b5a490b169b0414144ab" dependencies = [ "futures-core", "futures-task", @@ -2015,15 +2021,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" +checksum = "5fc94b64bb39543b4e432f1790b6bf18e3ee3b74653c5449f63310e9a74b123c" [[package]] name = "futures-macro" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +checksum = "f57ed14da4603b2554682e9f2ff3c65d7567b53188db96cb71538217fc64581b" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -2033,15 +2039,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" +checksum = "0d8764258ed64ebc5d9ed185cf86a95db5cac810269c5d20ececb32e0088abbd" [[package]] name = "futures-task" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +checksum = "4dd26820a9f3637f1302da8bceba3ff33adbe53464b54ca24d4e2d4f1db30f94" dependencies = [ "once_cell", ] @@ -2054,11 +2060,11 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +checksum = "8a894a0acddba51a2d49a6f4263b1e64b8c579ece8af50fa86503d52cd1eea34" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "futures-channel", "futures-core", "futures-io", @@ -2080,7 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce54d63f8b0c75023ed920d46fd71d0cbbb830b0ee012726b5b4f506fb6dea5b" dependencies = [ "bytes 0.5.6", - "futures 0.3.5", + "futures 0.3.6", "memchr", "pin-project", ] @@ -2120,7 +2126,7 @@ dependencies = [ "eth2_hashing", "eth2_ssz", "exit-future", - "futures 0.3.5", + "futures 0.3.6", "int_to_bytes", "merkle_proof", "parking_lot 0.11.0", @@ -2219,7 +2225,7 @@ dependencies = [ "byteorder", "bytes 0.4.12", "fnv", - "futures 0.1.29", + "futures 0.1.30", "http 0.1.21", "indexmap", "log 0.4.11", @@ -2259,7 +2265,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ - "ahash", + "ahash 0.3.8", "autocfg 1.0.1", ] @@ -2268,12 +2274,24 @@ name = "hashbrown" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.5", +] + +[[package]] +name = "hashlink" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" +dependencies = [ + "hashbrown 0.9.1", +] [[package]] name = "hashset_delay" version = "0.2.0" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "tokio 0.2.22", ] @@ -2313,9 +2331,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" dependencies = [ "libc", ] @@ -2418,7 +2436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "http 0.1.21", "tokio-buf", ] @@ -2532,7 +2550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "futures-cpupool", "h2 0.1.26", "http 0.1.21", @@ -2586,7 +2604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "hyper 0.12.35", "native-tls", "tokio-io", @@ -2767,7 +2785,7 @@ version = "14.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0747307121ffb9703afd93afbd0fb4f854c38fb873f2c8b90e0e902f27c7b62" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "log 0.4.11", "serde", "serde_derive", @@ -2813,7 +2831,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "0.2.13" +version = "0.3.0" dependencies = [ "bls", "clap", @@ -2826,7 +2844,7 @@ dependencies = [ "eth2_libp2p", "eth2_ssz", "eth2_testnet_config", - "futures 0.3.5", + "futures 0.3.6", "genesis", "hex 0.4.2", "lighthouse_version", @@ -2857,9 +2875,9 @@ dependencies = [ [[package]] name = "leveldb-sys" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c44b9b785ca705d58190ebd432a4e7edb900eadf236ff966d7d1307e482e87" +checksum = "618aee5ba3d32cb8456420a9a454aa71c1af5b3e9c7a2ec20a0f3cbbe47246cb" dependencies = [ "cmake", "libc", @@ -2903,7 +2921,7 @@ source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e95 dependencies = [ "atomic", "bytes 0.5.6", - "futures 0.3.5", + "futures 0.3.6", "lazy_static", "libp2p-core 0.22.2", "libp2p-core-derive", @@ -2934,7 +2952,7 @@ dependencies = [ "ed25519-dalek", "either", "fnv", - "futures 0.3.5", + "futures 0.3.6", "futures-timer", "lazy_static", "libsecp256k1", @@ -2967,7 +2985,7 @@ dependencies = [ "ed25519-dalek", "either", "fnv", - "futures 0.3.5", + "futures 0.3.6", "futures-timer", "lazy_static", "libsecp256k1", @@ -3004,7 +3022,7 @@ name = "libp2p-dns" version = "0.22.0" source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "libp2p-core 0.22.2", "log 0.4.11", ] @@ -3018,7 +3036,7 @@ dependencies = [ "byteorder", "bytes 0.5.6", "fnv", - "futures 0.3.5", + "futures 0.3.6", "futures_codec", "hex_fmt", "libp2p-core 0.22.2", @@ -3038,7 +3056,7 @@ name = "libp2p-identify" version = "0.22.0" source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "libp2p-core 0.22.2", "libp2p-swarm", "log 0.4.11", @@ -3055,7 +3073,7 @@ source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e95 dependencies = [ "bytes 0.5.6", "fnv", - "futures 0.3.5", + "futures 0.3.6", "futures_codec", "libp2p-core 0.22.2", "log 0.4.11", @@ -3070,7 +3088,7 @@ source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e95 dependencies = [ "bytes 0.5.6", "curve25519-dalek", - "futures 0.3.5", + "futures 0.3.6", "lazy_static", "libp2p-core 0.22.2", "log 0.4.11", @@ -3090,7 +3108,7 @@ version = "0.22.0" source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "either", - "futures 0.3.5", + "futures 0.3.6", "libp2p-core 0.22.2", "log 0.4.11", "rand 0.7.3", @@ -3104,7 +3122,7 @@ name = "libp2p-tcp" version = "0.22.0" source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "futures-timer", "get_if_addrs", "ipnet", @@ -3121,7 +3139,7 @@ source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e95 dependencies = [ "async-tls", "either", - "futures 0.3.5", + "futures 0.3.6", "libp2p-core 0.22.2", "log 0.4.11", "quicksink", @@ -3151,9 +3169,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a245984b1b06c291f46e27ebda9f369a94a1ab8461d0e845e23f9ced01f5db" +checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" dependencies = [ "cc", "pkg-config", @@ -3174,7 +3192,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "0.2.13" +version = "0.3.0" dependencies = [ "account_manager", "account_utils", @@ -3187,7 +3205,7 @@ dependencies = [ "env_logger", "environment", "eth2_testnet_config", - "futures 0.3.5", + "futures 0.3.6", "lighthouse_version", "logging", "slashing_protection", @@ -3279,15 +3297,6 @@ dependencies = [ "hashbrown 0.8.2", ] -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "lru_cache" version = "0.1.0" @@ -3403,9 +3412,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" dependencies = [ "adler", "autocfg 1.0.1", @@ -3533,7 +3542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9157e87afbc2ef0d84cc0345423d715f445edde00141c93721c162de35a05e5" dependencies = [ "bytes 0.5.6", - "futures 0.3.5", + "futures 0.3.6", "log 0.4.11", "pin-project", "smallvec 1.4.2", @@ -3546,7 +3555,7 @@ version = "0.8.3" source = "git+https://github.com/sigp/rust-libp2p?rev=5a9f0819af3990cfefad528e957297af596399b4#5a9f0819af3990cfefad528e957297af596399b4" dependencies = [ "bytes 0.5.6", - "futures 0.3.5", + "futures 0.3.6", "log 0.4.11", "pin-project", "smallvec 1.4.2", @@ -3593,7 +3602,7 @@ dependencies = [ "eth2_ssz_types", "exit-future", "fnv", - "futures 0.3.5", + "futures 0.3.6", "genesis", "get_if_addrs", "hashset_delay", @@ -3642,7 +3651,7 @@ dependencies = [ "environment", "eth2", "eth2_config", - "futures 0.3.5", + "futures 0.3.6", "genesis", "reqwest", "serde", @@ -4302,9 +4311,9 @@ checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" [[package]] name = "radium" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a333b5f6adeff5a89f2e95dc2ea1ecb5319abbb56212afea6a37f87435338a5" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "rand" @@ -4653,15 +4662,15 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c78c3275d9d6eb684d2db4b2388546b32fdae0586c20a82f3905d21ea78b9ef" +checksum = "7e3d4791ab5517217f51216a84a688b53c1ebf7988736469c538d02f46ddba68" dependencies = [ "bitflags 1.2.1", "fallible-iterator", "fallible-streaming-iterator", + "hashlink", "libsqlite3-sys", - "lru-cache", "memchr", "smallvec 1.4.2", ] @@ -4680,9 +4689,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +checksum = "b2610b7f643d18c87dff3b489950269617e6601a51f1f05aa5daefee36f64f0b" [[package]] name = "rustc-hash" @@ -4724,7 +4733,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4da5fcb054c46f5a5dff833b129285a93d3f0179531735e6c866e8cc307d2020" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "pin-project", "static_assertions", ] @@ -5070,7 +5079,7 @@ dependencies = [ "env_logger", "eth1", "eth1_test_rig", - "futures 0.3.5", + "futures 0.3.6", "node_test_rig", "parking_lot 0.11.0", "rayon", @@ -5274,7 +5283,7 @@ dependencies = [ "base64 0.12.3", "bytes 0.5.6", "flate2", - "futures 0.3.5", + "futures 0.3.6", "httparse", "log 0.4.11", "rand 0.7.3", @@ -5289,9 +5298,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "standback" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a71ea1ea5f8747d1af1979bfb7e65c3a025a70609f04ceb78425bc5adad8e6" +checksum = "f4e0831040d2cf2bdfd51b844be71885783d489898a192f254ae25d57cce725c" dependencies = [ "version_check 0.9.2", ] @@ -5504,7 +5513,7 @@ name = "task_executor" version = "0.1.0" dependencies = [ "exit-future", - "futures 0.3.5", + "futures 0.3.6", "lazy_static", "lighthouse_metrics", "slog", @@ -5573,18 +5582,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +checksum = "318234ffa22e0920fe9a40d7b8369b5f649d490980cf7aadcf1eb91594869b42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +checksum = "cae2447b6282786c3493999f40a9be2a6ad20cb8bd268b0a0dbf5a065535c0ab" dependencies = [ "proc-macro2", "quote", @@ -5663,7 +5672,7 @@ name = "timer" version = "0.2.0" dependencies = [ "beacon_chain", - "futures 0.3.5", + "futures 0.3.6", "parking_lot 0.11.0", "slog", "slot_clock", @@ -5729,7 +5738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "mio", "num_cpus", "tokio-codec", @@ -5778,7 +5787,7 @@ checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" dependencies = [ "bytes 0.4.12", "either", - "futures 0.1.29", + "futures 0.1.30", ] [[package]] @@ -5788,7 +5797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "tokio-io", ] @@ -5799,7 +5808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "iovec", "log 0.4.11", "mio", @@ -5817,7 +5826,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "tokio-executor", ] @@ -5828,7 +5837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ "crossbeam-utils", - "futures 0.1.29", + "futures 0.1.30", ] [[package]] @@ -5837,7 +5846,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "tokio-io", "tokio-threadpool", ] @@ -5849,7 +5858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "log 0.4.11", ] @@ -5881,7 +5890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" dependencies = [ "crossbeam-utils", - "futures 0.1.29", + "futures 0.1.30", "lazy_static", "log 0.4.11", "mio", @@ -5900,7 +5909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" dependencies = [ "fnv", - "futures 0.1.29", + "futures 0.1.30", ] [[package]] @@ -5910,7 +5919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "iovec", "mio", "tokio-io", @@ -5926,7 +5935,7 @@ dependencies = [ "crossbeam-deque", "crossbeam-queue", "crossbeam-utils", - "futures 0.1.29", + "futures 0.1.30", "lazy_static", "log 0.4.11", "num_cpus", @@ -5940,7 +5949,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6131e780037787ff1b3f8aad9da83bca02438b72277850dd6ad0d455e0e20efc" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "slab 0.3.0", ] @@ -5951,7 +5960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" dependencies = [ "crossbeam-utils", - "futures 0.1.29", + "futures 0.1.30", "slab 0.4.2", "tokio-executor", ] @@ -5962,7 +5971,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "354b8cd83825b3c20217a9dc174d6a0c67441a2fae5c41bcb1ea6679f6ae0f7c" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "native-tls", "tokio-io", ] @@ -5997,7 +6006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "log 0.4.11", "mio", "tokio-codec", @@ -6012,7 +6021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ae5d255ce739e8537221ed2942e0445f4b3b813daebac1c0050ddaaa3587f9" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "iovec", "libc", "log 0.3.9", @@ -6029,7 +6038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" dependencies = [ "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "iovec", "libc", "log 0.4.11", @@ -6394,7 +6403,7 @@ dependencies = [ [[package]] name = "validator_client" -version = "0.2.13" +version = "0.3.0" dependencies = [ "account_utils", "bincode", @@ -6412,7 +6421,7 @@ dependencies = [ "eth2_ssz", "eth2_ssz_derive", "exit-future", - "futures 0.3.5", + "futures 0.3.6", "hex 0.4.2", "hyper 0.13.8", "libc", @@ -6507,7 +6516,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" dependencies = [ - "futures 0.1.29", + "futures 0.1.30", "log 0.4.11", "try-lock", ] @@ -6529,7 +6538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f41be6df54c97904af01aa23e613d4521eed7ab23537cede692d4058f6449407" dependencies = [ "bytes 0.5.6", - "futures 0.3.5", + "futures 0.3.6", "headers", "http 0.2.1", "hyper 0.13.8", @@ -6674,7 +6683,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "js-sys", "parking_lot 0.11.0", "pin-utils", @@ -6704,7 +6713,7 @@ dependencies = [ "derive_more", "ethabi", "ethereum-types", - "futures 0.1.29", + "futures 0.1.30", "hyper 0.12.35", "hyper-tls 0.3.2", "jsonrpc-core", @@ -6755,7 +6764,7 @@ dependencies = [ "bitflags 0.9.1", "byteorder", "bytes 0.4.12", - "futures 0.1.29", + "futures 0.1.30", "hyper 0.10.16", "native-tls", "rand 0.5.6", @@ -6771,7 +6780,7 @@ dependencies = [ name = "websocket_server" version = "0.2.0" dependencies = [ - "futures 0.3.5", + "futures 0.3.6", "serde", "serde_derive", "slog", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index b935948e9..0eefabe46 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "account_manager" -version = "0.2.13" +version = "0.3.0" authors = ["Paul Hauner <paul@paulhauner.com>", "Luke Anderson <luke@sigmaprime.io>"] edition = "2018" diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 57a3fd977..83b25831b 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "0.2.13" +version = "0.3.0" authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com"] edition = "2018" diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 2c0ebdc68..d73f9cc13 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "0.2.13" +version = "0.3.0" authors = ["Sigma Prime <contact@sigmaprime.io>"] edition = "2018" diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 9696ac1d3..31258a2ad 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -111,7 +111,7 @@ define_net!(medalla, include_medalla_file, "medalla", true); define_net!(spadina, include_spadina_file, "spadina", true); -define_net!(zinken, include_zinken_file, "zinken", false); +define_net!(zinken, include_zinken_file, "zinken", true); #[cfg(test)] mod tests { diff --git a/common/eth2_testnet_config/testnet_zinken.zip b/common/eth2_testnet_config/testnet_zinken.zip index f81975f66c7dbe43c6bc95f3e8b088370e1c5fbc..1457638d10e28ef9a6a07b073547a1442f3230a6 100644 GIT binary patch literal 319297 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 -qGmpktYBtk->z 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 delta 341 zcmX@OPxvf%cz`!E3l{?j@HD#zx?IXAU<R^5n3q9@At^t<BtA8-NUt(6HzzcNlYyBd zur{7Gs5ZW|f}4Sn<poe7n23($0+}QN)W^YaPR4!mW+p}Umbo?Y3_-P<Z!;+~GhYV^ zGP0?$PXh}653HT6$EL*m5GWSRrp3M#DE0>^*1~2h!N?@XjLkvQ7u;o016e=4;2w+E z^auA?*i3;I1@^D-eaH!Ph9f%z12=;N&;bk#j6l4k(F;f>=jWwmrbC_2%D@bC5=g%c z$T`z3@3BZT&tPVlEXXFz29yLk4I&A2*Ytj%B-1PA>1%+rCdh$kJ`V6^Wdqs80)!zz J3zJzuJOEH3SGWKG diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index d88369793..c9abcf099 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -10,7 +10,7 @@ use target_info::Target; /// `Lighthouse/v0.2.0-1419501f2+` pub const VERSION: &str = git_version!( args = ["--always", "--dirty=+"], - prefix = "Lighthouse/v0.2.13-", + prefix = "Lighthouse/v0.3.0-", fallback = "unknown" ); diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index ea34a149c..57364aa18 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "0.2.13" +version = "0.3.0" authors = ["Paul Hauner <paul@paulhauner.com>"] edition = "2018" diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index ad1c359b7..4421d50eb 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "0.2.13" +version = "0.3.0" authors = ["Sigma Prime <contact@sigmaprime.io>"] edition = "2018" diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index a4035b731..873faca16 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "validator_client" -version = "0.2.13" +version = "0.3.0" authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>", "Luke Anderson <luke@lukeanderson.com.au>"] edition = "2018" From b0833033b7aad7ea20f3328f73f22c03d9915038 Mon Sep 17 00:00:00 2001 From: Michael Sproul <michael@sigmaprime.io> Date: Fri, 9 Oct 2020 02:05:32 +0000 Subject: [PATCH 31/32] Strict slashing protection by default (#1750) ## Proposed Changes Replace `--strict-slashing-protection` by `--init-slashing-protection` and remove mentions of `--auto-register` --- scripts/local_testnet/validator_client.sh | 2 +- testing/node_test_rig/src/lib.rs | 8 ++++ testing/simulator/src/eth1_sim.rs | 13 ++--- testing/simulator/src/no_eth1_sim.rs | 13 ++--- .../src/slashing_database.rs | 11 +++++ validator_client/src/cli.rs | 25 ++++------ validator_client/src/config.rs | 8 ++-- validator_client/src/http_api/tests.rs | 9 ++-- validator_client/src/lib.rs | 45 ++++++++++++++++-- validator_client/src/validator_store.rs | 47 ++++--------------- 10 files changed, 94 insertions(+), 87 deletions(-) diff --git a/scripts/local_testnet/validator_client.sh b/scripts/local_testnet/validator_client.sh index aab44045a..b68993637 100755 --- a/scripts/local_testnet/validator_client.sh +++ b/scripts/local_testnet/validator_client.sh @@ -15,4 +15,4 @@ exec lighthouse \ --datadir $VALIDATORS_DIR \ --secrets-dir $SECRETS_DIR \ --testnet-dir $TESTNET_DIR \ - --auto-register + --init-slashing-protection diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index e2391c0f8..2c2a0e81f 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -106,6 +106,14 @@ pub fn testing_client_config() -> ClientConfig { client_config } +pub fn testing_validator_config() -> ValidatorConfig { + ValidatorConfig { + init_slashing_protection: true, + disable_auto_discover: false, + ..ValidatorConfig::default() + } +} + /// Contains the directories for a `LocalValidatorClient`. /// /// This struct is separate to `LocalValidatorClient` to allow for pre-computation of validator diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/eth1_sim.rs index 75f2256a1..acf4fe845 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/eth1_sim.rs @@ -4,8 +4,8 @@ use eth1::http::Eth1NetworkId; use eth1_test_rig::GanacheEth1Instance; use futures::prelude::*; use node_test_rig::{ - environment::EnvironmentBuilder, testing_client_config, ClientGenesis, ValidatorConfig, - ValidatorFiles, + environment::EnvironmentBuilder, testing_client_config, testing_validator_config, + ClientGenesis, ValidatorFiles, }; use rayon::prelude::*; use std::net::{IpAddr, Ipv4Addr}; @@ -128,14 +128,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { */ for (i, files) in validator_files.into_iter().enumerate() { network - .add_validator_client( - ValidatorConfig { - disable_auto_discover: false, - ..ValidatorConfig::default() - }, - i, - files, - ) + .add_validator_client(testing_validator_config(), i, files) .await?; } diff --git a/testing/simulator/src/no_eth1_sim.rs b/testing/simulator/src/no_eth1_sim.rs index 621b8ef3b..afcd96986 100644 --- a/testing/simulator/src/no_eth1_sim.rs +++ b/testing/simulator/src/no_eth1_sim.rs @@ -2,8 +2,8 @@ use crate::{checks, LocalNetwork}; use clap::ArgMatches; use futures::prelude::*; use node_test_rig::{ - environment::EnvironmentBuilder, testing_client_config, ClientGenesis, ValidatorConfig, - ValidatorFiles, + environment::EnvironmentBuilder, testing_client_config, testing_validator_config, + ClientGenesis, ValidatorFiles, }; use rayon::prelude::*; use std::net::{IpAddr, Ipv4Addr}; @@ -99,14 +99,7 @@ pub fn run_no_eth1_sim(matches: &ArgMatches) -> Result<(), String> { let add_validators_fut = async { for (i, files) in validator_files.into_iter().enumerate() { network - .add_validator_client( - ValidatorConfig { - disable_auto_discover: false, - ..ValidatorConfig::default() - }, - i, - files, - ) + .add_validator_client(testing_validator_config(), i, files) .await?; } diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index df0b38ecf..1bfdcf60d 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -173,6 +173,17 @@ impl SlashingDatabase { Ok(()) } + /// Check that all of the given validators are registered. + pub fn check_validator_registrations<'a>( + &self, + mut public_keys: impl Iterator<Item = &'a PublicKey>, + ) -> Result<(), NotSafe> { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + public_keys + .try_for_each(|public_key| self.get_validator_id_in_txn(&txn, public_key).map(|_| ())) + } + /// Get the database-internal ID for a validator. /// /// This is NOT the same as a validator index, and depends on the ordering that validators diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 0651bf536..c78979880 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -52,14 +52,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .conflicts_with("datadir") .requires("validators-dir"), ) - .arg(Arg::with_name("auto-register").long("auto-register").help( - "If present, the validator client will register any new signing keys with \ - the slashing protection database so that they may be used. WARNING: \ - enabling the same signing key on multiple validator clients WILL lead to \ - that validator getting slashed. Only use this flag the first time you run \ - the validator client, or if you're certain there are no other \ - nodes using the same key. Automatically enabled unless `--strict` is specified", - )) .arg( Arg::with_name("delete-lockfiles") .long("delete-lockfiles") @@ -73,14 +65,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) ) .arg( - Arg::with_name("strict-slashing-protection") - .long("strict-slashing-protection") - .help( - "If present, do not create a new slashing database. This is to ensure that users \ - do not accidentally get slashed in case their slashing protection db ends up in the \ - wrong directory during directory restructure and vc creates a new empty db and \ - re-registers all validators." - ) + Arg::with_name("init-slashing-protection") + .long("init-slashing-protection") + .help( + "If present, do not require the slashing protection database to exist before \ + running. You SHOULD NOT use this flag unless you're certain that a new \ + slashing protection database is required. Usually, your database \ + will have been initialized when you imported your validator keys. If you \ + misplace your database and then run with this flag you risk being slashed." + ) ) .arg( Arg::with_name("disable-auto-discover") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index d51f44e44..f26eaf9e0 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -32,8 +32,8 @@ pub struct Config { pub delete_lockfiles: bool, /// If true, don't scan the validators dir for new keystores. pub disable_auto_discover: bool, - /// If true, don't re-register existing validators in definitions.yml for slashing protection. - pub strict_slashing_protection: bool, + /// If true, re-register existing validators in definitions.yml for slashing protection. + pub init_slashing_protection: bool, /// Graffiti to be inserted everytime we create a block. pub graffiti: Option<Graffiti>, /// Configuration for the HTTP REST API. @@ -58,7 +58,7 @@ impl Default for Config { allow_unsynced_beacon_node: false, delete_lockfiles: false, disable_auto_discover: false, - strict_slashing_protection: false, + init_slashing_protection: false, graffiti: None, http_api: <_>::default(), } @@ -122,7 +122,7 @@ impl Config { config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced"); config.delete_lockfiles = cli_args.is_present("delete-lockfiles"); config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); - config.strict_slashing_protection = cli_args.is_present("strict-slashing-protection"); + config.init_slashing_protection = cli_args.is_present("init-slashing-protection"); if let Some(input_graffiti) = cli_args.value_of("graffiti") { let graffiti_bytes = input_graffiti.as_bytes(); diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index e9344b5f4..eef3aa8ae 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -17,6 +17,7 @@ use eth2::{ }; use eth2_keystore::KeystoreBuilder; use parking_lot::RwLock; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slot_clock::TestingSlotClock; use std::marker::PhantomData; use std::net::Ipv4Addr; @@ -65,15 +66,17 @@ impl ApiTester { .build() .unwrap(); + let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); + let validator_store: ValidatorStore<TestingSlotClock, E> = ValidatorStore::new( initialized_validators, - &config, + slashing_protection, Hash256::repeat_byte(42), E::default_spec(), fork_service.clone(), log.clone(), - ) - .unwrap(); + ); let initialized_validators = validator_store.initialized_validators(); diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 3b7dc6b07..5591a6cbf 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -28,6 +28,7 @@ use futures::channel::mpsc; use http_api::ApiSecret; use initialized_validators::InitializedValidators; use notifier::spawn_notifier; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slog::{error, info, Logger}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; @@ -106,6 +107,44 @@ impl<T: EthSpec> ProductionValidatorClient<T> { .await .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; + // Initialize slashing protection. + let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = if config.init_slashing_protection { + SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| { + format!( + "Failed to open or create slashing protection database: {:?}", + e + ) + }) + } else { + SlashingDatabase::open(&slashing_db_path).map_err(|e| { + format!( + "Failed to open slashing protection database: {:?}.\n\ + Ensure that `slashing_protection.sqlite` is in {:?} folder", + e, config.validator_dir + ) + }) + }?; + + // Check validator registration with slashing protection, or auto-register all validators. + if config.init_slashing_protection { + slashing_protection + .register_validators(validators.iter_voting_pubkeys()) + .map_err(|e| format!("Error while registering slashing protection: {:?}", e))?; + } else { + slashing_protection + .check_validator_registrations(validators.iter_voting_pubkeys()) + .map_err(|e| { + format!( + "One or more validators not found in slashing protection database.\n\ + Ensure you haven't misplaced your slashing protection database, or \ + carefully consider running with --init-slashing-protection (see --help). \ + Error: {:?}", + e + ) + })?; + } + info!( log, "Initialized validators"; @@ -157,12 +196,12 @@ impl<T: EthSpec> ProductionValidatorClient<T> { let validator_store: ValidatorStore<SystemTimeSlotClock, T> = ValidatorStore::new( validators, - &config, + slashing_protection, genesis_validators_root, context.eth2_config.spec.clone(), fork_service.clone(), log.clone(), - )?; + ); info!( log, @@ -170,8 +209,6 @@ impl<T: EthSpec> ProductionValidatorClient<T> { "voting_validators" => validator_store.num_voting_validators() ); - validator_store.register_all_validators_for_slashing_protection()?; - let duties_service = DutiesServiceBuilder::new() .slot_clock(slot_clock.clone()) .validator_store(validator_store.clone()) diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 08583f84d..66b75874a 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -1,9 +1,7 @@ -use crate::{ - config::Config, fork_service::ForkService, initialized_validators::InitializedValidators, -}; +use crate::{fork_service::ForkService, initialized_validators::InitializedValidators}; use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; use parking_lot::RwLock; -use slashing_protection::{NotSafe, Safe, SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{NotSafe, Safe, SlashingDatabase}; use slog::{crit, error, warn, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; @@ -56,32 +54,13 @@ pub struct ValidatorStore<T, E: EthSpec> { impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { pub fn new( validators: InitializedValidators, - config: &Config, + slashing_protection: SlashingDatabase, genesis_validators_root: Hash256, spec: ChainSpec, fork_service: ForkService<T>, log: Logger, - ) -> Result<Self, String> { - let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); - let slashing_protection = if config.strict_slashing_protection { - // Don't create a new slashing database if `strict_slashing_protection` is turned on. - SlashingDatabase::open(&slashing_db_path).map_err(|e| { - format!( - "Failed to open slashing protection database: {:?}. - Ensure that `slashing_protection.sqlite` is in {:?} folder", - e, config.validator_dir - ) - })? - } else { - SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| { - format!( - "Failed to open or create slashing protection database: {:?}", - e - ) - })? - }; - - Ok(Self { + ) -> Self { + Self { validators: Arc::new(RwLock::new(validators)), slashing_protection, genesis_validators_root, @@ -90,7 +69,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { temp_dir: None, fork_service, _phantom: PhantomData, - }) + } } pub fn initialized_validators(&self) -> Arc<RwLock<InitializedValidators>> { @@ -130,16 +109,6 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { Ok(validator_def) } - /// Register all known validators with the slashing protection database. - /// - /// Registration is required to protect against a lost or missing slashing database, - /// such as when relocating validator keys to a new machine. - pub fn register_all_validators_for_slashing_protection(&self) -> Result<(), String> { - self.slashing_protection - .register_validators(self.validators.read().iter_voting_pubkeys()) - .map_err(|e| format!("Error while registering validators: {:?}", e)) - } - pub fn voting_pubkeys(&self) -> Vec<PublicKey> { self.validators .read() @@ -235,7 +204,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { warn!( self.log, "Not signing block for unregistered validator"; - "msg" => "Carefully consider running with --auto-register (see --help)", + "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); None @@ -314,7 +283,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { warn!( self.log, "Not signing attestation for unregistered validator"; - "msg" => "Carefully consider running with --auto-register (see --help)", + "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); None From 0e4cc502626ad98d8311d79033063511ce16734b Mon Sep 17 00:00:00 2001 From: Paul Hauner <paul@paulhauner.com> Date: Fri, 9 Oct 2020 15:56:28 +1100 Subject: [PATCH 32/32] Remove unused deps --- Cargo.lock | 2 -- beacon_node/eth2_libp2p/Cargo.toml | 1 - beacon_node/network/Cargo.toml | 1 - 3 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2a6a4726..17184a10f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1689,7 +1689,6 @@ dependencies = [ "sha2 0.9.1", "slog", "slog-async", - "slog-stdlog", "slog-term", "smallvec 1.4.2", "snap", @@ -3597,7 +3596,6 @@ name = "network" version = "0.2.0" dependencies = [ "beacon_chain", - "environment", "error-chain", "eth2_libp2p", "eth2_ssz", diff --git a/beacon_node/eth2_libp2p/Cargo.toml b/beacon_node/eth2_libp2p/Cargo.toml index 69c7cab92..82cf8cf79 100644 --- a/beacon_node/eth2_libp2p/Cargo.toml +++ b/beacon_node/eth2_libp2p/Cargo.toml @@ -48,7 +48,6 @@ features = ["websocket", "identify", "mplex", "noise", "gossipsub", "dns", "tcp- [dev-dependencies] tokio = { version = "0.2.22", features = ["full"] } -slog-stdlog = "4.0.0" slog-term = "2.6.0" slog-async = "2.5.0" tempdir = "0.3.7" diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 469e5fc03..c2d81bf9d 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -35,7 +35,6 @@ fnv = "1.0.7" rlp = "0.4.6" lazy_static = "1.4.0" lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -environment = { path = "../../lighthouse/environment" } task_executor = { path = "../../common/task_executor" } igd = "0.11.1" itertools = "0.9.0"