diff --git a/Makefile b/Makefile index 91a7401..d000b1c 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ all: build: $(MAKE) -C lean_client build +.PHONY: clean +clean: + $(MAKE) -C lean_client clean + .PHONY: format format: $(MAKE) -C lean_client format diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 1203b29..32b5dc7 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -175,7 +190,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -186,7 +201,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -194,6 +209,9 @@ name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +dependencies = [ + "backtrace", +] [[package]] name = "arithmetic" @@ -552,13 +570,77 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + [[package]] name = "backend" version = "0.3.0" source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" dependencies = [ "fiat-shamir", - "itertools 0.14.0", + "itertools 0.10.5", "p3-field 0.3.0", "p3-util 0.3.0", "rand 0.9.2", @@ -566,6 +648,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base-x" version = "0.2.11" @@ -642,6 +739,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -672,6 +778,66 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bls" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "bls-blst", + "bls-core", +] + +[[package]] +name = "bls-blst" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "bls-core", + "blst", + "derivative", + "derive_more", + "fixed-hash", + "hex", + "impl-serde 0.5.0", + "itertools 0.14.0", + "once_cell", + "rand 0.8.5", + "serde", + "serde_utils", + "ssz", + "static_assertions", + "typenum", + "zeroize", +] + +[[package]] +name = "bls-core" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "derivative", + "hex", + "once_cell", + "serde", + "ssz", + "static_assertions", + "thiserror 2.0.17", + "typenum", + "zeroize", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + [[package]] name = "bs58" version = "0.4.0" @@ -912,6 +1078,7 @@ dependencies = [ "anyhow", "env-config", "hex", + "metrics", "pretty_assertions", "rstest", "serde", @@ -1303,6 +1470,17 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[package]] +name = "duplicate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" +dependencies = [ + "heck", + "proc-macro2 1.0.103", + "proc-macro2-diagnostics", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1422,6 +1600,46 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -1459,7 +1677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1471,7 +1689,7 @@ dependencies = [ "crunchy", "fixed-hash", "impl-rlp", - "impl-serde", + "impl-serde 0.4.0", "tiny-keccak", ] @@ -1484,7 +1702,7 @@ dependencies = [ "ethbloom", "fixed-hash", "impl-rlp", - "impl-serde", + "impl-serde 0.4.0", "primitive-types", "uint 0.9.5", ] @@ -1566,6 +1784,20 @@ dependencies = [ "bytes", ] +[[package]] +name = "features" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "enum-iterator", + "log", + "logging", + "parse-display", + "serde", + "tracing", + "variant_count", +] + [[package]] name = "ff" version = "0.13.1" @@ -1636,6 +1868,7 @@ dependencies = [ "anyhow", "containers", "env-config", + "metrics", "rand 0.9.2", "rand_chacha 0.9.0", "serde", @@ -1837,6 +2070,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -2061,12 +2300,54 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http_api" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "clap", + "futures", + "metrics", + "tokio", + "tower-http", + "tracing", +] + +[[package]] +name = "http_api_utils" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "anyhow", + "axum", + "features", + "http", + "http-body-util", + "itertools 0.14.0", + "logging", + "mime", + "parse-display", + "prometheus_metrics", + "thiserror 2.0.17", + "tower", + "tower-http", + "tracing", + "types", +] + [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -2081,6 +2362,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -2296,6 +2578,20 @@ dependencies = [ "xmltree", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -2323,6 +2619,15 @@ dependencies = [ "serde", ] +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -2499,13 +2804,18 @@ dependencies = [ name = "lean_client" version = "0.1.0" dependencies = [ + "anyhow", + "bls", "chain", "clap", "containers", "ethereum-types", + "features", "fork_choice", "hex", + "http_api", "libp2p-identity 0.2.13", + "metrics", "networking", "ssz", "tokio", @@ -3082,6 +3392,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logging" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "derive_more", + "tracing", +] + [[package]] name = "lookup" version = "0.1.0" @@ -3132,18 +3451,56 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "metrics" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "clap", + "http_api_utils", + "itertools 0.14.0", + "once_cell", + "prometheus", + "thiserror 2.0.17", + "tokio", + "tower-http", + "tracing", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.1.1" @@ -3389,6 +3746,7 @@ dependencies = [ "libp2p", "libp2p-identity 0.2.13", "libp2p-mplex", + "metrics", "num-bigint", "num-traits", "parking_lot", @@ -3431,13 +3789,19 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3501,12 +3865,21 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 1.1.3", "proc-macro2 1.0.103", "quote 1.0.42", "syn 2.0.111", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -3948,6 +4321,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parse-display" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287d8d3ebdce117b8539f59411e4ed9ec226e0a4153c7f55495c6070d68e6f72" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc048687be30d79502dea2f623d052f3a074012c6eac41726b7ab17213616b1" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.111", +] + [[package]] name = "paste" version = "1.0.15" @@ -4141,7 +4539,7 @@ dependencies = [ "fixed-hash", "impl-codec", "impl-rlp", - "impl-serde", + "impl-serde 0.4.0", "uint 0.9.5", ] @@ -4206,6 +4604,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", + "version_check", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 2.0.17", +] + [[package]] name = "prometheus-client" version = "0.23.1" @@ -4229,6 +4654,23 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "prometheus_metrics" +version = "0.1.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "anyhow", + "features", + "futures", + "logging", + "once_cell", + "prometheus", + "tokio", + "tokio-stream", + "tracing", + "types", +] + [[package]] name = "proptest" version = "1.9.0" @@ -4248,6 +4690,26 @@ dependencies = [ "unarray", ] +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -4432,6 +4894,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rapidhash" version = "4.1.1" @@ -4694,6 +5165,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4743,7 +5220,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4939,6 +5416,29 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_utils" version = "0.0.0" @@ -5075,6 +5575,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.11" @@ -5086,6 +5596,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "snap" @@ -5228,6 +5741,50 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "structmeta-derive", + "syn 2.0.111", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + [[package]] name = "sub_protocols" version = "0.1.0" @@ -5295,6 +5852,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.12.6" @@ -5361,7 +5924,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5425,6 +5988,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" version = "0.3.44" @@ -5518,6 +6090,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -5571,6 +6155,44 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -5701,6 +6323,43 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "types" +version = "0.0.0" +source = "git+https://github.com/grandinetech/grandine?rev=64afdee3c6be79fceffb66933dcb69a943f3f1ae#64afdee3c6be79fceffb66933dcb69a943f3f1ae" +dependencies = [ + "anyhow", + "arithmetic", + "bit_field", + "bls", + "derivative", + "derive_more", + "duplicate", + "enum-iterator", + "enum-map", + "ethereum-types", + "generic-array", + "hex", + "hex-literal", + "im", + "itertools 0.14.0", + "nonzero_ext", + "once_cell", + "primitive-types", + "serde", + "serde_utils", + "serde_with", + "smallvec", + "ssz", + "static_assertions", + "std_ext", + "strum", + "thiserror 2.0.17", + "typenum", + "url", + "variant_count", +] + [[package]] name = "ucd-trie" version = "0.1.7" @@ -5859,6 +6518,7 @@ dependencies = [ "env-config", "ethereum-types", "fork_choice", + "metrics", "serde_yaml", "ssz", "tracing", @@ -5873,6 +6533,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "variant_count" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1935e10c6f04d22688d07c0790f2fc0e1b1c5c2c55bc0cc87ed67656e587dd8" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6468,6 +7139,7 @@ dependencies = [ "hex", "lean-multisig", "leansig", + "metrics", "rand 0.9.2", "rand_chacha 0.9.0", "serde", @@ -6592,6 +7264,7 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ + "serde", "zeroize_derive", ] diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 9ea212d..a681d0f 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -1,5 +1,15 @@ [workspace] -members = ["chain", "containers", "env-config", "fork_choice", "networking", "validator", "xmss"] +members = [ + "chain", + "containers", + "env-config", + "fork_choice", + "http_api", + "metrics", + "networking", + "validator", + "xmss", +] resolver = "3" [workspace.package] @@ -210,16 +220,20 @@ missing_panics_doc = { level = 'allow', priority = 1 } private_intra_doc_links = 'allow' [workspace.dependencies] -xmss = { path = "./xmss" } -env-config = { path = "./env-config" } chain = { path = "./chain" } containers = { path = "./containers" } +env-config = { path = "./env-config" } fork_choice = { path = "./fork_choice" } +http_api = { path = "./http_api" } +metrics = { path = "./metrics" } networking = { path = "./networking" } validator = { path = "./validator" } +xmss = { path = "./xmss" } anyhow = "1.0.100" async-trait = "0.1" +axum = "0.8.8" +bls = { git = "https://github.com/grandinetech/grandine", package = "bls", features = ["blst"], rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } clap = { version = "4", features = ["derive"] } derive_more = "2.1.1" discv5 = "0.10.2" @@ -227,7 +241,9 @@ enr = { version = "0.13", features = ["k256"] } eth_ssz = { package = "ethereum_ssz", version = "0.10.0" } ethereum-types = "0.14" futures = "0.3" +features = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } hex = "0.4.3" +http_api_utils = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } k256 = "0.13" lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", rev = "e4474138487eeb1ed7c2e1013674fe80ac9f3165" } leansig = { git = "https://github.com/leanEthereum/leanSig", rev = "73bedc26ed961b110df7ac2e234dc11361a4bf25" } @@ -247,9 +263,11 @@ libp2p-identity = { version = "0.2", features = ["secp256k1"] } libp2p-mplex = "0.39" num-bigint = "0.4" num-traits = "0.2" +once_cell = "1.21" parking_lot = "0.12" paste = "1.0.15" pretty_assertions = "1.4" +prometheus = "0.14" rand = "0.9" rand_chacha = "0.9" rstest = "0.18" @@ -259,10 +277,13 @@ serde_yaml = "0.9" sha2 = "0.10" snap = "1.1" ssz = { git = "https://github.com/grandinetech/grandine", package = "ssz", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" } -ssz-types = "0.3.0" +ssz-types = "0.3" +itertools = "0.14" test-generator = "0.3.1" +thiserror = "2" tiny-keccak = "2.0.2" tokio = { version = "1.0", features = ["full"] } +tower-http = { version = '0.6', features = ['cors', 'trace'] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } tree-hash = "0.4.0" @@ -277,17 +298,22 @@ version = "0.1.0" edition = { workspace = true } [dependencies] +anyhow = { workspace = true } +bls = { workspace = true } chain = { workspace = true } clap = { workspace = true } containers = { workspace = true } ethereum-types = { workspace = true } +features = { workspace = true } fork_choice = { workspace = true } hex = { workspace = true } +http_api = { workspace = true } libp2p-identity = { workspace = true } +metrics = { workspace = true } networking = { workspace = true } ssz = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } validator = { workspace = true } -xmss = { workspace = true } \ No newline at end of file +xmss = { workspace = true } diff --git a/lean_client/Makefile b/lean_client/Makefile index 9072046..1083f65 100644 --- a/lean_client/Makefile +++ b/lean_client/Makefile @@ -57,10 +57,27 @@ generate-test-vectors: rm -rf ./test_vectors && mkdir -p ./test_vectors cp -r ./spec/fixtures/consensus/* ./test_vectors/ +.PHONY: generate-test-vectors +generate-test-vectors: + @rm -rf spec && \ + mkdir -p spec && \ + cd spec && \ + git init && \ + git remote add origin https://github.com/leanEthereum/leanSpec.git && \ + git fetch --depth 1 origin $(LEAN_SPEC_COMMIT) && \ + git switch --detach FETCH_HEAD + cd spec && uv run fill --clean --fork=devnet + cp -r ./spec/fixtures/consensus/* ./tests/test_vectors/ + .PHONY: build build: cargo build --release +.PHONY: clean +clean: + cargo clean + rm -rf ./bin + .PHONY: x86_64-unknown-linux-gnu x86_64-unknown-linux-gnu: ./target/x86_64-unknown-linux-gnu/release/lean_client diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index bdb9c3f..c8e6a2d 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } anyhow = { workspace = true } env-config = { workspace = true } hex = { workspace = true } +metrics = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 95e2cb0..2bad3a8 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,5 +1,6 @@ use crate::{Attestation, Slot, State}; -use anyhow::{Context, Result, anyhow, ensure}; +use anyhow::{Context, Result, ensure}; +use metrics::METRICS; use serde::{Deserialize, Serialize}; use ssz::{H256, Ssz, SszHash}; use xmss::Signature; @@ -199,6 +200,12 @@ impl SignedBlockWithAttestation { proposer_attestation.validator_id ))?; + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_pq_signature_attestation_verification_time_seconds + .start_timer() + }); + proposer_signature .verify( &proposer.pubkey, diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 5e9ee18..d354262 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result, ensure}; +use metrics::METRICS; use serde::{Deserialize, Serialize}; use ssz::{BitList, H256, PersistentList, Ssz, SszHash}; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -229,6 +230,9 @@ impl State { ) -> Result { ensure!(valid_signatures, "invalid block signatures"); + let _timer = METRICS + .get() + .map(|metrics| metrics.lean_state_transition_time_seconds.start_timer()); let block = &signed_block.message.block; let mut state = self.process_slots(block.slot)?; state = state.process_block(block)?; @@ -246,11 +250,21 @@ impl State { pub fn process_slots(&self, target_slot: Slot) -> Result { ensure!(self.slot < target_slot, "target slot must be in the future"); + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_state_transition_slots_processing_time_seconds + .start_timer() + }); + let mut state = self.clone(); while state.slot < target_slot { state = state.process_slot(); state.slot = Slot(state.slot.0 + 1); + + METRICS + .get() + .map(|metrics| metrics.lean_state_transition_slots_processed_total.inc()); } Ok(state) @@ -275,6 +289,12 @@ impl State { } pub fn process_block(&self, block: &Block) -> Result { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_state_transition_block_processing_time_seconds + .start_timer() + }); + let state = self.process_block_header(block)?; ensure!( @@ -387,6 +407,12 @@ impl State { } pub fn process_attestations(&self, attestations: &AggregatedAttestations) -> Self { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_state_transition_attestations_processing_time_seconds + .start_timer() + }); + let mut justifications = self.get_justifications(); let mut latest_justified = self.latest_justified.clone(); let mut latest_finalized = self.latest_finalized.clone(); @@ -405,6 +431,11 @@ impl State { } for aggregated_attestation in attestations { + METRICS.get().map(|metrics| { + metrics + .lean_state_transition_attestations_processed_total + .inc() + }); let validator_ids = aggregated_attestation .aggregation_bits .to_validator_indices(); @@ -726,6 +757,17 @@ impl State { aggregated_payloads, )?; + METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_attestations_in_aggregated_signatures_total + .inc_by( + aggregated_attestations + .iter() + .map(|v| v.aggregation_bits.to_validator_indices().len()) + .sum::() as u64, + ); + }); + let mut final_block = Block { slot, proposer_index, diff --git a/lean_client/fork_choice/Cargo.toml b/lean_client/fork_choice/Cargo.toml index 59fb10a..bde97cd 100644 --- a/lean_client/fork_choice/Cargo.toml +++ b/lean_client/fork_choice/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } anyhow = { workspace = true } containers = { workspace = true } env-config = { workspace = true } +metrics = { workspace = true } ssz = { workspace = true } tracing = { workspace = true } xmss = { workspace = true } diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 73c5224..3aa3d6e 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -1,6 +1,8 @@ use anyhow::{Result, anyhow, bail, ensure}; use containers::{AttestationData, SignatureKey, SignedAttestation, SignedBlockWithAttestation}; +use metrics::{METRICS, stop_and_discard}; use ssz::{H256, SszHash}; +use tracing::warn; use crate::store::{INTERVALS_PER_SLOT, SECONDS_PER_INTERVAL, Store, tick_interval, update_head}; @@ -95,11 +97,24 @@ pub fn on_gossip_attestation( store: &mut Store, signed_attestation: SignedAttestation, ) -> Result<()> { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_attestation_validation_time_seconds + .start_timer() + }); + let validator_id = signed_attestation.validator_id; let attestation_data = signed_attestation.message.clone(); // Validate the attestation data first - validate_attestation_data(store, &attestation_data)?; + validate_attestation_data(store, &attestation_data).inspect_err(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_attestations_invalid_total + .with_label_values(&["gossip"]) + .inc() + }); + })?; // Store signature for later lookup during block building let data_root = attestation_data.hash_tree_root(); @@ -110,6 +125,22 @@ pub fn on_gossip_attestation( // Process the attestation data (not from block) on_attestation_internal(store, validator_id, attestation_data, false) + .inspect_err(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_attestations_invalid_total + .with_label_values(&["gossip"]) + .inc() + }); + }) + .inspect(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_attestations_valid_total + .with_label_values(&["gossip"]) + .inc() + }); + }) } /// Process an attestation and place it into the correct attestation stage @@ -126,11 +157,24 @@ pub fn on_attestation( signed_attestation: SignedAttestation, is_from_block: bool, ) -> Result<()> { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_attestation_validation_time_seconds + .start_timer() + }); + let validator_id = signed_attestation.validator_id; let attestation_data = signed_attestation.message.clone(); // Validate attestation data - validate_attestation_data(store, &attestation_data)?; + validate_attestation_data(store, &attestation_data).inspect_err(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_attestations_invalid_total + .with_label_values(&[if is_from_block { "block" } else { "gossip" }]) + .inc() + }); + })?; if !is_from_block { // Store signature for later aggregation during block building @@ -142,6 +186,22 @@ pub fn on_attestation( } on_attestation_internal(store, validator_id, attestation_data, is_from_block) + .inspect_err(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_attestations_invalid_total + .with_label_values(&[if is_from_block { "block" } else { "gossip" }]) + .inc() + }); + }) + .inspect(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_attestations_valid_total + .with_label_values(&[if is_from_block { "block" } else { "gossip" }]) + .inc() + }); + }) } /// Internal attestation processing - stores AttestationData @@ -197,6 +257,7 @@ pub fn on_block(store: &mut Store, signed_block: SignedBlockWithAttestation) -> let block_root = signed_block.message.block.hash_tree_root(); if store.blocks.contains_key(&block_root) { + // stop_and_discard(timer); return Ok(()); } @@ -226,6 +287,12 @@ fn process_block_internal( signed_block: SignedBlockWithAttestation, block_root: H256, ) -> Result<()> { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_fork_choice_block_processing_time_seconds + .start_timer() + }); + let block = signed_block.message.block.clone(); let attestations_count = block.body.attestations.len_u64(); @@ -271,6 +338,13 @@ fn process_block_internal( "Store justified checkpoint updated!" ); store.latest_justified = new_state.latest_justified.clone(); + METRICS.get().map(|metrics| { + let Some(slot) = new_state.latest_justified.slot.0.try_into().ok() else { + warn!("unable to set latest_justified slot in metrics"); + return; + }; + metrics.lean_latest_justified_slot.set(slot); + }); } if finalized_updated { tracing::info!( @@ -279,6 +353,13 @@ fn process_block_internal( "Store finalized checkpoint updated!" ); store.latest_finalized = new_state.latest_finalized.clone(); + METRICS.get().map(|metrics| { + let Some(slot) = new_state.latest_finalized.slot.0.try_into().ok() else { + warn!("unable to set latest_finalized slot in metrics"); + return; + }; + metrics.lean_latest_finalized_slot.set(slot); + }); } if !justified_updated && !finalized_updated { diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 39e08a7..7ec73aa 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -5,6 +5,7 @@ use containers::{ AggregatedSignatureProof, Attestation, AttestationData, Block, Checkpoint, Config, SignatureKey, SignedBlockWithAttestation, Slot, State, }; +use metrics::set_gauge_u64; use ssz::{H256, SszHash}; use xmss::Signature; @@ -188,6 +189,18 @@ pub fn update_head(store: &mut Store) { 0, ); store.head = new_head; + + set_gauge_u64( + |m| &m.lean_head_slot, + || { + let head = store + .blocks + .get(&new_head) + .ok_or(anyhow!("failed to get head block"))?; + + Ok(head.slot.0) + }, + ); } pub fn update_safe_target(store: &mut Store) { @@ -199,8 +212,21 @@ pub fn update_safe_target(store: &mut Store) { let min_score = (n_validators * 2 + 2) / 3; let root = store.latest_justified.root; - store.safe_target = + let new_safe_target = get_fork_choice_head(store, root, &store.latest_new_attestations, min_score); + store.safe_target = new_safe_target; + + set_gauge_u64( + |metrics| &metrics.lean_safe_target_slot, + || { + let safe_target = store + .blocks + .get(&new_safe_target) + .ok_or(anyhow!("failed to get safe target block"))?; + + Ok(safe_target.slot.0) + }, + ); } pub fn accept_new_attestations(store: &mut Store) { diff --git a/lean_client/http_api/Cargo.toml b/lean_client/http_api/Cargo.toml new file mode 100644 index 0000000..d5a2524 --- /dev/null +++ b/lean_client/http_api/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = 'http_api' +version = '0.1.0' +edition = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true } +clap = { workspace = true } +futures = { workspace = true } +metrics = { workspace = true } +tokio = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true } diff --git a/lean_client/http_api/src/config.rs b/lean_client/http_api/src/config.rs new file mode 100644 index 0000000..3b2d3e3 --- /dev/null +++ b/lean_client/http_api/src/config.rs @@ -0,0 +1,34 @@ +use core::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use anyhow::Result; +use clap::Args; +use metrics::MetricsServerConfig; +use tokio::net::TcpListener; + +const DEFAULT_HTTP_PORT: u16 = 8080; + +#[derive(Debug, Clone, Args)] +pub struct HttpServerConfig { + #[clap(long = "http-address", default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))] + http_address: IpAddr, + + #[clap(long = "http-port", default_value_t = DEFAULT_HTTP_PORT)] + http_port: u16, + + #[clap(flatten)] + pub(crate) metrics: MetricsServerConfig, +} + +impl HttpServerConfig { + pub(crate) async fn listener(&self) -> Result { + TcpListener::bind(self.address()).await.map_err(Into::into) + } + + pub fn address(&self) -> SocketAddr { + (self.http_address, self.http_port).into() + } + + pub fn metrics_enabled(&self) -> bool { + self.metrics.enabled() + } +} diff --git a/lean_client/http_api/src/lib.rs b/lean_client/http_api/src/lib.rs new file mode 100644 index 0000000..e269386 --- /dev/null +++ b/lean_client/http_api/src/lib.rs @@ -0,0 +1,6 @@ +mod config; +mod routing; +mod server; + +pub use config::HttpServerConfig; +pub use server::run_server; diff --git a/lean_client/http_api/src/routing.rs b/lean_client/http_api/src/routing.rs new file mode 100644 index 0000000..cdc7e22 --- /dev/null +++ b/lean_client/http_api/src/routing.rs @@ -0,0 +1,14 @@ +use axum::Router; +use metrics::metrics_module; + +use crate::config::HttpServerConfig; + +pub fn normal_routes(config: &HttpServerConfig) -> Router { + let mut router = Router::new(); + + if config.metrics_enabled() { + router = router.merge(metrics_module(config.metrics.clone())); + } + + router +} diff --git a/lean_client/http_api/src/server.rs b/lean_client/http_api/src/server.rs new file mode 100644 index 0000000..09142d6 --- /dev/null +++ b/lean_client/http_api/src/server.rs @@ -0,0 +1,26 @@ +use core::net::SocketAddr; + +use anyhow::{Context, Error as AnyhowError, Result}; +use futures::{TryFutureExt as _, future::FutureExt as _}; +use tracing::info; + +use crate::{config::HttpServerConfig, routing::normal_routes}; + +pub async fn run_server(config: HttpServerConfig) -> Result<()> { + let router = normal_routes(&config); + + let listener = config + .listener() + .await + .context("failed to start http server")?; + + let service = router.into_make_service_with_connect_info::(); + + let serve_requests = axum::serve(listener, service) + .into_future() + .map_err(AnyhowError::new); + + info!("HTTP server listening on {}", config.address()); + + serve_requests.fuse().await +} diff --git a/lean_client/metrics/Cargo.toml b/lean_client/metrics/Cargo.toml new file mode 100644 index 0000000..6b402aa --- /dev/null +++ b/lean_client/metrics/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = 'metrics' +version = '0.1.0' +edition = { workspace = true } + +[dependencies] +axum = { workspace = true, features = ['macros'] } +anyhow = { workspace = true } +clap = { workspace = true } +http_api_utils = { workspace = true } +prometheus = { workspace = true } +once_cell = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +itertools = { workspace = true } +tokio = { workspace = true } +tower-http = { workspace = true } diff --git a/lean_client/metrics/src/helpers.rs b/lean_client/metrics/src/helpers.rs new file mode 100644 index 0000000..5f27930 --- /dev/null +++ b/lean_client/metrics/src/helpers.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use prometheus::{HistogramTimer, IntGauge, core::Collector}; + +use crate::{METRICS, Metrics}; + +#[inline] +pub fn set_gauge_u64( + metric_getter: impl FnOnce(&Arc) -> &IntGauge, + value_getter: impl FnOnce() -> Result, +) { + METRICS.get().map(|v| { + let metric = metric_getter(v); + + let _ = value_getter() + .and_then(|value| { + metric.set(value.try_into().context("invalid metric value")?); + Ok(()) + }) + .inspect_err(|err| { + let desc = metric.desc(); + + tracing::warn!( + "failed to set metric {metric_name} with error: {err:?}", + metric_name = desc + .get(0) + .map(|v| v.fq_name.clone()) + .unwrap_or("".to_owned()) + ) + }); + }); +} + +pub fn stop_and_record(timer: Option) { + if let Some(timer) = timer { + timer.stop_and_record(); + } +} + +pub fn stop_and_discard(timer: Option) { + if let Some(timer) = timer { + timer.stop_and_discard(); + } +} diff --git a/lean_client/metrics/src/lib.rs b/lean_client/metrics/src/lib.rs new file mode 100644 index 0000000..7c23a9a --- /dev/null +++ b/lean_client/metrics/src/lib.rs @@ -0,0 +1,7 @@ +mod helpers; +mod metrics; +mod server; + +pub use helpers::{set_gauge_u64, stop_and_discard, stop_and_record}; +pub use metrics::{DisconnectReason, METRICS, Metrics}; +pub use server::{MetricsServerConfig, metrics_module}; diff --git a/lean_client/metrics/src/metrics.rs b/lean_client/metrics/src/metrics.rs new file mode 100644 index 0000000..f0dee42 --- /dev/null +++ b/lean_client/metrics/src/metrics.rs @@ -0,0 +1,447 @@ +use std::{sync::Arc, time::SystemTime}; + +use anyhow::{Context, Result}; +use once_cell::sync::OnceCell; +use prometheus::{ + GaugeVec, Histogram, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, histogram_opts, opts, +}; + +pub static METRICS: OnceCell> = OnceCell::new(); + +#[derive(Debug)] +pub struct Metrics { + /// Node information: name and version + lean_node_info: GaugeVec, + + /// Start timestamp + lean_node_start_time_seconds: IntGauge, + + // PQ Signature metrics + /// Time taken to sign an attestation + pub lean_pq_signature_attestation_signing_time_seconds: Histogram, + + /// Time taken to verify an attestation signature + pub lean_pq_signature_attestation_verification_time_seconds: Histogram, + + /// Total number of aggregated signatures + pub lean_pq_sig_aggregated_signatures_total: IntCounter, + + /// Total number of attestations included into aggregated signatures + pub lean_pq_sig_attestations_in_aggregated_signatures_total: IntCounter, + + /// Time taken to build an aggregated attestation signature + pub lean_pq_sig_attestation_signatures_building_time_seconds: Histogram, + + /// Time taken to verify an aggregated attestation signature + pub lean_pq_sig_aggregated_signatures_verification_time_seconds: Histogram, + + /// Total number of valid aggregated signatures + pub lean_pq_sig_aggregated_signatures_valid_total: IntCounter, + + /// Total number of invalid aggregated signatures + pub lean_pq_sig_aggregated_signatures_invalid_total: IntCounter, + + // Fork-Choice Metrics + /// Latest slot of the lean chain + pub lean_head_slot: IntGauge, + + /// Current slot of the lean chain + lean_current_slot: IntGauge, + + /// Safe target slot + pub lean_safe_target_slot: IntGauge, + + /// Time taken to process block + pub lean_fork_choice_block_processing_time_seconds: Histogram, + + /// Total number of valid attestations + pub lean_attestations_valid_total: IntCounterVec, + + /// Total number of invalid attestations + pub lean_attestations_invalid_total: IntCounterVec, + + /// Time taken to validate attestation + pub lean_attestation_validation_time_seconds: Histogram, + + /// Total number of fork choice reorgs + lean_fork_choice_reorgs_total: IntCounter, + + /// Depth of fork choice reorgs (in blocks) + lean_fork_choice_reorg_depth: Histogram, + + // State Transition Metrics + /// Latest justified slot + pub lean_latest_justified_slot: IntGauge, + + /// Latest finalized slot + pub lean_latest_finalized_slot: IntGauge, + + /// Total number of finalization attempts + lean_finalizations_total: IntCounterVec, + + /// Time to process state transition + pub lean_state_transition_time_seconds: Histogram, + + /// Total number of processed slots + pub lean_state_transition_slots_processed_total: IntCounter, + + /// Time taken to process slots + pub lean_state_transition_slots_processing_time_seconds: Histogram, + + /// Time taken to process block + pub lean_state_transition_block_processing_time_seconds: Histogram, + + /// Total number of processed attestations + pub lean_state_transition_attestations_processed_total: IntCounter, + + /// Time taken to process attestations + pub lean_state_transition_attestations_processing_time_seconds: Histogram, + + // Validator metrics + /// Number of validators managed by a node + pub lean_validators_count: IntGauge, + + // Network Metrics + /// Number of connected peers + pub lean_connected_peers: IntGaugeVec, + + /// Total number of peer connection events + lean_peer_connection_events_total: IntCounterVec, + + /// Total number of peer disconnection events + lean_peer_disconnection_events_total: IntCounterVec, +} + +impl Metrics { + pub fn new() -> Result { + Ok(Self { + lean_node_info: GaugeVec::new( + opts!("lean_node_info", "Node information"), + &["name", "version"], + )?, + lean_node_start_time_seconds: IntGauge::new( + "lean_node_start_time_seconds", + "Start timestamp", + )?, + + // PQ Signature metrics + lean_pq_signature_attestation_signing_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_pq_signature_attestation_signing_time_seconds", + "Time taken to sign an attestation", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0], + ), + )?, + lean_pq_signature_attestation_verification_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_pq_signature_attestation_verification_time_seconds", + "Time taken to verify an attestation signature", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0], + ), + )?, + lean_pq_sig_aggregated_signatures_total: IntCounter::new( + "lean_pq_sig_aggregated_signatures_total", + "Total number of aggregated signatures", + )?, + lean_pq_sig_attestations_in_aggregated_signatures_total: IntCounter::new( + "lean_pq_sig_attestations_in_aggregated_signatures_total", + "Total number of attestations included into aggregated signatures", + )?, + lean_pq_sig_attestation_signatures_building_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_pq_sig_attestation_signatures_building_time_seconds", + "Time taken to verify an aggregated attestation signature", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ), + )?, + lean_pq_sig_aggregated_signatures_verification_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_pq_sig_aggregated_signatures_verification_time_seconds", + "Time taken to verify an aggregated attestation signature", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ), + )?, + lean_pq_sig_aggregated_signatures_valid_total: IntCounter::new( + "lean_pq_sig_aggregated_signatures_valid_total", + "On validate aggregated signature", + )?, + lean_pq_sig_aggregated_signatures_invalid_total: IntCounter::new( + "lean_pq_sig_aggregated_signatures_invalid_total", + "Total number of invalid aggregated signatures", + )?, + + // Fork-Choice Metrics + lean_head_slot: IntGauge::new("lean_head_slot", "Latest slot of the lean chain")?, + lean_current_slot: IntGauge::new( + "lean_current_slot", + "Current slot of the lean chain", + )?, + lean_safe_target_slot: IntGauge::new("lean_safe_target_slot", "Safe target slot")?, + lean_fork_choice_block_processing_time_seconds: Histogram::with_opts(histogram_opts!( + "lean_fork_choice_block_processing_time_seconds", + "Time taken to process block", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ))?, + lean_attestations_valid_total: IntCounterVec::new( + opts!( + "lean_attestations_valid_total", + "Total number of valid attestations", + ), + &["source"], + )?, + lean_attestations_invalid_total: IntCounterVec::new( + opts!( + "lean_attestations_invalid_total", + "Total number of invalid attestations", + ), + &["source"], + )?, + lean_attestation_validation_time_seconds: Histogram::with_opts(histogram_opts!( + "lean_attestation_validation_time_seconds", + "Time taken to validate attestation", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ))?, + lean_fork_choice_reorgs_total: IntCounter::new( + "lean_fork_choice_reorgs_total", + "Total number of fork choice reorgs", + )?, + lean_fork_choice_reorg_depth: Histogram::with_opts(histogram_opts!( + "lean_fork_choice_reorg_depth", + "Depth of fork choice reorgs (in blocks)", + vec![1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0, 50.0, 100.0] + ))?, + + // State Transition Metrics + lean_latest_justified_slot: IntGauge::new( + "lean_latest_justified_slot", + "Latest justified slot", + )?, + lean_latest_finalized_slot: IntGauge::new( + "lean_latest_finalized_slot", + "Latest finalized slot", + )?, + lean_finalizations_total: IntCounterVec::new( + opts!( + "lean_finalizations_total", + "Total number of finalization attempts", + ), + &["result"], + )?, + lean_state_transition_time_seconds: Histogram::with_opts(histogram_opts!( + "lean_state_transition_time_seconds", + "Time to process state transition", + vec![0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0] + ))?, + lean_state_transition_slots_processed_total: IntCounter::new( + "lean_state_transition_slots_processed_total", + "Total number of processed slots", + )?, + lean_state_transition_slots_processing_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_state_transition_slots_processing_time_seconds", + "Time taken to process slots", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ), + )?, + lean_state_transition_block_processing_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_state_transition_block_processing_time_seconds", + "Time taken to process block", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ), + )?, + lean_state_transition_attestations_processed_total: IntCounter::new( + "lean_state_transition_attestations_processed_total", + "Total number of processed attestations", + )?, + lean_state_transition_attestations_processing_time_seconds: Histogram::with_opts( + histogram_opts!( + "lean_state_transition_attestations_processing_time_seconds", + " Time taken to process attestations", + vec![0.005, 0.01, 0.025, 0.05, 0.1, 1.0] + ), + )?, + + // Validator metrics + lean_validators_count: IntGauge::new( + "lean_validators_count", + "Number of validators managed by a node", + )?, + + // Network Metrics + lean_connected_peers: IntGaugeVec::new( + opts!("lean_connected_peers", "Number of connected peers",), + &["client"], + )?, + lean_peer_connection_events_total: IntCounterVec::new( + opts!( + "lean_peer_connection_events_total", + "Total number of peer connection events", + ), + &["direction", "result"], + )?, + lean_peer_disconnection_events_total: IntCounterVec::new( + opts!( + "lean_peer_disconnection_events_total", + "Total number of peer disconnection events", + ), + &["direction", "reason"], + )?, + }) + } + + pub fn register_with_default_metrics(&self) -> Result<()> { + let default_registry = prometheus::default_registry(); + + default_registry.register(Box::new(self.lean_node_info.clone()))?; + default_registry.register(Box::new(self.lean_node_start_time_seconds.clone()))?; + default_registry.register(Box::new( + self.lean_pq_signature_attestation_signing_time_seconds + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_signature_attestation_verification_time_seconds + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_sig_aggregated_signatures_total.clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_sig_attestations_in_aggregated_signatures_total + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_sig_attestation_signatures_building_time_seconds + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_sig_aggregated_signatures_verification_time_seconds + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_sig_aggregated_signatures_valid_total.clone(), + ))?; + default_registry.register(Box::new( + self.lean_pq_sig_aggregated_signatures_invalid_total.clone(), + ))?; + default_registry.register(Box::new(self.lean_head_slot.clone()))?; + default_registry.register(Box::new(self.lean_current_slot.clone()))?; + default_registry.register(Box::new(self.lean_safe_target_slot.clone()))?; + default_registry.register(Box::new( + self.lean_fork_choice_block_processing_time_seconds.clone(), + ))?; + default_registry.register(Box::new(self.lean_attestations_valid_total.clone()))?; + default_registry.register(Box::new(self.lean_attestations_invalid_total.clone()))?; + default_registry.register(Box::new( + self.lean_attestation_validation_time_seconds.clone(), + ))?; + default_registry.register(Box::new(self.lean_fork_choice_reorgs_total.clone()))?; + default_registry.register(Box::new(self.lean_fork_choice_reorg_depth.clone()))?; + default_registry.register(Box::new(self.lean_latest_justified_slot.clone()))?; + default_registry.register(Box::new(self.lean_latest_finalized_slot.clone()))?; + default_registry.register(Box::new(self.lean_finalizations_total.clone()))?; + default_registry.register(Box::new(self.lean_state_transition_time_seconds.clone()))?; + default_registry.register(Box::new( + self.lean_state_transition_slots_processed_total.clone(), + ))?; + default_registry.register(Box::new( + self.lean_state_transition_slots_processing_time_seconds + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_state_transition_block_processing_time_seconds + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_state_transition_attestations_processed_total + .clone(), + ))?; + default_registry.register(Box::new( + self.lean_state_transition_attestations_processing_time_seconds + .clone(), + ))?; + default_registry.register(Box::new(self.lean_validators_count.clone()))?; + default_registry.register(Box::new(self.lean_connected_peers.clone()))?; + default_registry.register(Box::new(self.lean_peer_connection_events_total.clone()))?; + default_registry.register(Box::new(self.lean_peer_disconnection_events_total.clone()))?; + + Ok(()) + } + + pub fn set_client_version(&self, name: String, version: String) { + self.lean_node_info + .with_label_values(&[name, version]) + .set(1.0); + } + + pub fn set_start_time(&self, timestamp: SystemTime) -> Result<()> { + let timestamp = timestamp + .duration_since(SystemTime::UNIX_EPOCH) + .context("failed to calculate timestamp")? + .as_secs() + .try_into() + .context("sorry, grandine support ended ~292 billion years ago")?; + self.lean_node_start_time_seconds.set(timestamp); + + Ok(()) + } + + /// Increments successfull peer connection event count metric. + pub fn register_peer_connection_success(&self, is_inbound: bool) -> Result<()> { + let direction = if is_inbound { "inbound" } else { "outbound" }; + let metric = self + .lean_peer_connection_events_total + .get_metric_with_label_values(&[direction, "success"])?; + metric.inc(); + Ok(()) + } + + /// Increments peer connection failure event count metric. + pub fn register_peer_connection_failure(&self, is_inbound: bool) -> Result<()> { + let direction = if is_inbound { "inbound" } else { "outbound" }; + let metric = self + .lean_peer_connection_events_total + .get_metric_with_label_values(&[direction, "failure"])?; + metric.inc(); + Ok(()) + } + + /// Increments peer connection timeout event count metric. + pub fn register_peer_connection_timeout(&self, is_inbound: bool) -> Result<()> { + let direction = if is_inbound { "inbound" } else { "outbound" }; + let metric = self + .lean_peer_connection_events_total + .get_metric_with_label_values(&[direction, "timeout"])?; + metric.inc(); + Ok(()) + } + + pub fn register_peer_disconnect( + &self, + is_inbound: bool, + reason: DisconnectReason, + ) -> Result<()> { + let direction = if is_inbound { "inbound" } else { "outbound" }; + let reason = match reason { + DisconnectReason::Timeout => "timeout", + DisconnectReason::RemoteClose => "remote_close", + DisconnectReason::LocalClose => "local_close", + DisconnectReason::Error => "error", + }; + let metric = self + .lean_peer_disconnection_events_total + .get_metric_with_label_values(&[direction, reason])?; + + metric.inc(); + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub enum DisconnectReason { + Timeout, + RemoteClose, + LocalClose, + Error, +} diff --git a/lean_client/metrics/src/server.rs b/lean_client/metrics/src/server.rs new file mode 100644 index 0000000..9ee30e4 --- /dev/null +++ b/lean_client/metrics/src/server.rs @@ -0,0 +1,88 @@ +use std::{error::Error as StdError, time::Duration}; + +use anyhow::{Error as AnyhowError, Result}; +use axum::{ + Router, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, +}; +use clap::Args; +use http_api_utils::ApiError; +use prometheus::TextEncoder; +use thiserror::Error; +use tower_http::cors::AllowOrigin; + +#[derive(Clone, Debug, Args)] +pub struct MetricsServerConfig { + #[arg(long = "metrics-timeout", default_value_t = Self::default().timeout, requires = "metrics_enabled")] + timeout: u64, + + #[arg(long = "metrics")] + metrics_enabled: bool, +} + +impl Default for MetricsServerConfig { + fn default() -> Self { + Self { + metrics_enabled: false, + timeout: Duration::from_secs(1000) + .as_millis() + .try_into() + .expect("should fit into u64"), + } + } +} + +impl MetricsServerConfig { + pub fn enabled(&self) -> bool { + self.metrics_enabled + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("internal error")] + Internal(#[from] AnyhowError), +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } +} + +impl ApiError for Error { + fn sources(&self) -> impl Iterator { + let mut error: Option<&dyn StdError> = Some(self); + + core::iter::from_fn(move || { + let source = error?.source(); + core::mem::replace(&mut error, source) + }) + } +} + +pub fn metrics_module(config: MetricsServerConfig) -> Router { + let router = Router::new().route("/metrics", get(get_metrics)); + + let router = http_api_utils::extend_router_with_middleware::( + router, + Some(Duration::from_millis(config.timeout)), + AllowOrigin::any(), + None, + ); + + router +} + +/// `GET /metrics` +async fn get_metrics() -> Result { + let mut buffer = String::new(); + + TextEncoder::new() + .encode_utf8(prometheus::gather().as_slice(), &mut buffer) + .map_err(AnyhowError::new)?; + + Ok(buffer) +} diff --git a/lean_client/networking/Cargo.toml b/lean_client/networking/Cargo.toml index d477a05..1a53517 100644 --- a/lean_client/networking/Cargo.toml +++ b/lean_client/networking/Cargo.toml @@ -16,6 +16,7 @@ k256 = { workspace = true } libp2p = { workspace = true } libp2p-identity = { workspace = true } libp2p-mplex = { workspace = true } +metrics = { workspace = true } parking_lot = { workspace = true } rand = { workspace = true } serde = { workspace = true } diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 3dd6624..0af5417 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -1,10 +1,13 @@ use std::{ collections::HashMap, fs::File, + io, net::IpAddr, num::{NonZeroU8, NonZeroUsize}, - sync::Arc, - sync::atomic::{AtomicU64, Ordering}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, }; use anyhow::{Result, anyhow}; @@ -17,9 +20,10 @@ use libp2p::{ gossipsub::{Event, IdentTopic, MessageAuthenticity}, identify, multiaddr::Protocol, - swarm::{Config, Swarm, SwarmEvent}, + swarm::{Config, ConnectionError, Swarm, SwarmEvent}, }; use libp2p_identity::{Keypair, PeerId}; +use metrics::{DisconnectReason, METRICS}; use parking_lot::Mutex; use rand::seq::IndexedRandom; use serde::{Deserialize, Serialize}; @@ -324,18 +328,20 @@ where event: SwarmEvent, ) -> Option { match event { - SwarmEvent::Behaviour(LeanNetworkBehaviourEvent::Gossipsub(event)) => { - self.handle_gossipsub_event(event).await - } - SwarmEvent::Behaviour(LeanNetworkBehaviourEvent::ReqResp(event)) => { - self.handle_request_response_event(event) - } - SwarmEvent::Behaviour(LeanNetworkBehaviourEvent::Identify(event)) => { - self.handle_identify_event(event) - } - SwarmEvent::Behaviour(_) => { - // ConnectionLimits behaviour has no events - None + SwarmEvent::Behaviour(event) => { + match event { + LeanNetworkBehaviourEvent::Gossipsub(event) => { + self.handle_gossipsub_event(event).await + } + LeanNetworkBehaviourEvent::ReqResp(event) => { + self.handle_request_response_event(event) + } + LeanNetworkBehaviourEvent::Identify(event) => self.handle_identify_event(event), + LeanNetworkBehaviourEvent::ConnectionLimits(_) => { + // ConnectionLimits behaviour has no events + None + } + } } SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. @@ -358,9 +364,18 @@ where self.send_status_request(peer_id); } + METRICS.get().map(|metrics| { + metrics.register_peer_connection_success(endpoint.is_listener()) + }); + None } - SwarmEvent::ConnectionClosed { peer_id, .. } => { + SwarmEvent::ConnectionClosed { + peer_id, + cause, + endpoint, + .. + } => { self.peer_table .lock() .insert(peer_id, ConnectionState::Disconnected); @@ -373,7 +388,24 @@ where .count() as u64; self.peer_count.store(connected, Ordering::Relaxed); - info!(peer = %peer_id, "Disconnected from peer (total: {})", connected); + info!(peer = %peer_id, ?cause, "Disconnected from peer (total: {})", connected); + + METRICS.get().map(|metrics| { + let reason = match cause { + None => DisconnectReason::LocalClose, + Some(ConnectionError::IO(io_error)) => match io_error.kind() { + io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset => { + DisconnectReason::RemoteClose + } + io::ErrorKind::TimedOut => DisconnectReason::Timeout, + _ => DisconnectReason::Error, + }, + Some(ConnectionError::KeepAliveTimeout) => DisconnectReason::Timeout, + }; + + metrics.register_peer_disconnect(endpoint.is_listener(), reason) + }); + Some(NetworkEvent::PeerDisconnected(peer_id)) } SwarmEvent::IncomingConnection { local_addr, .. } => { @@ -384,10 +416,6 @@ where info!(?peer_id, "Dialing peer"); peer_id.map(NetworkEvent::PeerConnectedOutgoing) } - SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { - warn!(?peer_id, ?error, "Failed to connect to peer"); - None - } SwarmEvent::NewListenAddr { listener_id, address, @@ -409,6 +437,28 @@ where info!(?address, "External address expired"); None } + SwarmEvent::IncomingConnectionError { + send_back_addr, + error, + .. + } => { + warn!(?error, ?send_back_addr, "Incoming connection error"); + + METRICS + .get() + .map(|metrics| metrics.register_peer_connection_failure(true)); + + None + } + SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { + warn!(?peer_id, ?error, "Failed to connect to peer"); + + METRICS + .get() + .map(|metrics| metrics.register_peer_connection_failure(false)); + + None + } _ => { info!(?event, "Unhandled swarm event"); None diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index 5706365..ff1b732 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -3,9 +3,11 @@ use std::{collections::HashMap, fmt::Display}; use anyhow::{Result, anyhow}; use async_trait::async_trait; use containers::{SignedAttestation, SignedBlockWithAttestation}; +use metrics::METRICS; use serde::{Deserialize, Serialize}; use ssz::H256; use tokio::sync::mpsc; +use tracing::warn; use crate::serde_utils::quoted_u64; @@ -104,6 +106,20 @@ impl PeerCount { ConnectionState::Disconnecting => count.disconnecting += 1, } } + + METRICS.get().map(|metrics| { + let Ok(connected) = count.connected.try_into() else { + warn!("failed to set connected pear count metric"); + return; + }; + + // TODO(metrics): actual client names should be provided into with_label_values + metrics + .lean_connected_peers + .with_label_values(&["unknown"]) + .set(connected); + }); + count } } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 02b55c4..fe39572 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -1,14 +1,18 @@ +use anyhow::{Context as _, Result}; use clap::Parser; use containers::{ Attestation, AttestationData, Block, BlockBody, BlockSignatures, BlockWithAttestation, Checkpoint, Config, SignedBlockWithAttestation, Slot, State, Validator, }; use ethereum_types::H256; +use features::Feature; use fork_choice::{ handlers::{on_attestation, on_block, on_tick}, store::{INTERVALS_PER_SLOT, Store, get_forkchoice_store}, }; +use http_api::HttpServerConfig; use libp2p_identity::Keypair; +use metrics::{METRICS, Metrics, MetricsServerConfig}; use networking::gossipsub::config::GossipsubConfig; use networking::gossipsub::topic::get_topics; use networking::network::{NetworkService, NetworkServiceConfig}; @@ -24,7 +28,7 @@ use tokio::{ time::{Duration, interval}, }; use tracing::level_filters::LevelFilter; -use tracing::{debug, info, warn}; +use tracing::{debug, error, info, warn}; use validator::{ValidatorConfig, ValidatorService}; use xmss::{PublicKey, Signature}; @@ -125,10 +129,17 @@ struct Args { /// Path: directory containing XMSS validator keys (validator_N_sk.ssz files) #[arg(long)] hash_sig_key_dir: Option, + + #[command(flatten)] + http_config: HttpServerConfig, + + /// List of optional runtime features to enable + #[clap(long, value_delimiter = ',')] + features: Vec, } #[tokio::main] -async fn main() { +async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::builder() @@ -139,6 +150,29 @@ async fn main() { let args = Args::parse(); + for feature in args.features { + feature.enable(); + } + + let metrics = if args.http_config.metrics_enabled() { + let metrics = Metrics::new()?; + metrics.register_with_default_metrics()?; + let metrics = Arc::new(metrics); + METRICS.get_or_init(|| metrics.clone()); + + Some(metrics) + } else { + None + }; + + metrics + .map(|metrics| { + metrics.set_client_version("grandine".to_owned(), "0.0.0".to_owned()); + metrics.set_start_time(SystemTime::now()) + }) + .transpose() + .context("failed to set metrics on start")?; + let (outbound_p2p_sender, outbound_p2p_receiver) = mpsc::unbounded_channel::(); let (chain_message_sender, mut chain_message_receiver) = @@ -343,6 +377,12 @@ async fn main() { let chain_outbound_sender = outbound_p2p_sender.clone(); + let http_handle = task::spawn(async move { + if let Err(err) = http_api::run_server(args.http_config).await { + error!("HTTP Server failed with error: {err:?}"); + } + }); + let chain_handle = task::spawn(async move { let mut tick_interval = interval(Duration::from_millis(1000)); let mut last_logged_slot = 0u64; @@ -561,12 +601,17 @@ async fn main() { tokio::select! { _ = network_handle => { - println!("Network service finished."); + info!("Network service finished."); } _ = chain_handle => { - println!("Chain service finished."); + info!("Chain service finished."); + } + _ = http_handle => { + info!("Http service finished."); } } - println!("Main async task exiting..."); + info!("Main async task exiting..."); + + Ok(()) } diff --git a/lean_client/validator/Cargo.toml b/lean_client/validator/Cargo.toml index 6c7152b..e819550 100644 --- a/lean_client/validator/Cargo.toml +++ b/lean_client/validator/Cargo.toml @@ -6,11 +6,12 @@ edition = { workspace = true } anyhow = { workspace = true } containers = { workspace = true } env-config = { workspace = true } +ethereum-types = { workspace = true } fork_choice = { workspace = true } +metrics = { workspace = true } serde_yaml = { workspace = true } +ssz = { workspace = true } tracing = { workspace = true } typenum = { workspace = true } -ethereum-types = { workspace = true } -ssz = { workspace = true } xmss = { workspace = true } zeroize = { workspace = true } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 63a9d0e..17fe3b4 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -9,6 +9,7 @@ use containers::{ }; use ethereum_types::H256; use fork_choice::store::{Store, get_proposal_head, get_vote_target}; +use metrics::METRICS; use ssz::SszHash; use tracing::{info, warn}; @@ -27,16 +28,13 @@ pub struct ValidatorConfig { impl ValidatorConfig { // load validator index - pub fn load_from_file( - path: impl AsRef, - node_id: &str, - ) -> Result> { + pub fn load_from_file(path: impl AsRef, node_id: &str) -> Result { let file = std::fs::File::open(path)?; let registry: ValidatorRegistry = serde_yaml::from_reader(file)?; let indices = registry .get(node_id) - .ok_or_else(|| format!("Node '{}' not found in validator registry", node_id))? + .ok_or_else(|| anyhow!("Node `{node_id}` not found in validator registry"))? .clone(); info!(node_id = %node_id, indices = ?indices, "Validator config loaded..."); @@ -66,6 +64,13 @@ impl ValidatorService { total_validators = num_validators, "VALIDATOR INITIALIZED SUCCESSFULLY" ); + + METRICS.get().map(|metrics| { + metrics + .lean_validators_count + .set(config.validator_indices.len() as i64) + }); + Self { config, num_validators, @@ -93,6 +98,12 @@ impl ValidatorService { "VALIDATOR INITIALIZED WITH XMSS KEYS" ); + METRICS.get().map(|metrics| { + metrics + .lean_validators_count + .set(config.validator_indices.len() as i64) + }); + Ok(Self { config, num_validators, @@ -297,6 +308,12 @@ impl ValidatorService { let proposer_signature: Signature; if let Some(ref key_manager) = self.key_manager { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_pq_signature_attestation_signing_time_seconds + .start_timer() + }); + // Sign proposer attestation with XMSS let message = proposer_attestation.hash_tree_root(); let epoch = slot.0 as u32; @@ -381,6 +398,12 @@ impl ValidatorService { }; let signature = if let Some(ref key_manager) = self.key_manager { + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_pq_signature_attestation_signing_time_seconds + .start_timer() + }); + // Sign with XMSS let message = attestation.hash_tree_root(); let epoch = slot.0 as u32; diff --git a/lean_client/xmss/Cargo.toml b/lean_client/xmss/Cargo.toml index dd85368..123365d 100644 --- a/lean_client/xmss/Cargo.toml +++ b/lean_client/xmss/Cargo.toml @@ -13,6 +13,7 @@ ethereum-types = { workspace = true } hex = { workspace = true } lean-multisig = { workspace = true } leansig = { workspace = true } +metrics = { workspace = true } rand = { workspace = true } ssz = { workspace = true } typenum = { workspace = true } diff --git a/lean_client/xmss/src/aggregated_signature.rs b/lean_client/xmss/src/aggregated_signature.rs index 28c9825..e549991 100644 --- a/lean_client/xmss/src/aggregated_signature.rs +++ b/lean_client/xmss/src/aggregated_signature.rs @@ -9,6 +9,7 @@ use lean_multisig::{ Devnet2XmssAggregateSignature, xmss_aggregate_signatures, xmss_aggregation_setup_prover, xmss_aggregation_setup_verifier, xmss_verify_aggregated_signatures, }; +use metrics::{METRICS, stop_and_discard}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use ssz::{ByteList, Ssz}; use typenum::U1048576; @@ -60,6 +61,12 @@ impl AggregatedSignature { ) -> Result { setup_prover(); + let timer = METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_attestation_signatures_building_time_seconds + .start_timer() + }); + let public_keys = public_keys .into_iter() .map(|k| k.as_lean()) @@ -70,6 +77,7 @@ impl AggregatedSignature { .collect::>(); if public_keys.len() != signatures.len() { + stop_and_discard(timer); bail!( "public key & signature count mismatch ({} != {})", public_keys.len(), @@ -81,6 +89,12 @@ impl AggregatedSignature { xmss_aggregate_signatures(&public_keys, &signatures, message.as_fixed_bytes(), epoch) .map_err(|err| anyhow!("{err:?}"))?; + METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_aggregated_signatures_total + .inc_by(signatures.len() as u64) + }); + Ok(Self(aggregate.as_ssz_bytes().try_into()?)) } @@ -92,6 +106,12 @@ impl AggregatedSignature { ) -> Result<()> { setup_verifier(); + let _timer = METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_aggregated_signatures_verification_time_seconds + .start_timer() + }); + let public_keys = public_keys .into_iter() .map(|k| k.as_lean()) @@ -106,6 +126,18 @@ impl AggregatedSignature { epoch, ) .map_err(|err| anyhow!("{err:?}")) + .inspect(|_| { + METRICS + .get() + .map(|metrics| metrics.lean_pq_sig_aggregated_signatures_valid_total.inc()); + }) + .inspect_err(|_| { + METRICS.get().map(|metrics| { + metrics + .lean_pq_sig_aggregated_signatures_invalid_total + .inc() + }); + }) } fn as_lean(&self) -> Devnet2XmssAggregateSignature {