diff --git a/.ci/.env b/.ci/.env index 8730f5d..0d81340 100644 --- a/.ci/.env +++ b/.ci/.env @@ -1,9 +1,9 @@ GCC_VERSION=14 +RUST_VERSION=stable LINUX_PACKAGES="make \ autoconf \ automake \ build-essential \ - cargo \ curl \ gcc-$GCC_VERSION \ g++-$GCC_VERSION \ diff --git a/.ci/scripts/init.sh b/.ci/scripts/init.sh index 9c19d2b..ed9273a 100755 --- a/.ci/scripts/init.sh +++ b/.ci/scripts/init.sh @@ -15,6 +15,12 @@ main() { apt update && apt install -y $LINUX_PACKAGES update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-$GCC_VERSION 90 update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-$GCC_VERSION 90 + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 90 + # Install Rust via rustup + if ! command -v rustup >/dev/null 2>&1; then + echo "=== Installing Rust ${RUST_VERSION} via rustup..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${RUST_VERSION} --profile minimal + fi if [ -f "$HOME/.cargo/env" ]; then source "$HOME/.cargo/env" fi diff --git a/.ci/scripts/init_py.sh b/.ci/scripts/init_py.sh index 7894fbc..d5068c5 100755 --- a/.ci/scripts/init_py.sh +++ b/.ci/scripts/init_py.sh @@ -4,7 +4,23 @@ set -euo pipefail init_py() { echo "$VENV" + # Verify python3 is available and check version + if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is not available" + exit 1 + fi + echo "Using Python: $(python3 --version)" + + # Create venv python3 -m venv "$VENV" + + # Verify venv was created successfully + if [ ! -f "$VENV/bin/python3" ]; then + echo "Error: venv created but python3 binary is missing" + echo "This might indicate that python3-venv package is not installed" + exit 1 + fi + source $VENV/bin/activate pip3 install cmake==${CMAKE_VERSION} pip3 install --no-cache-dir asn1tools diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 571d33e..d31037e 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -23,6 +23,7 @@ env: RUSTUP_HOME: ~/.rustup CACHE_PATH: | ~/.cargo + ~/.rustup ~/.hunter ~/.cache/pip ~/.cache/vcpkg @@ -45,12 +46,6 @@ jobs: submodules: true fetch-depth: 0 - - name: "Set up Rust" - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - components: rustfmt, clippy - - name: "Restore cache dependencies" id: cache-restore if: ${{ env.USE_CACHE == 'true' }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e84e63e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,99 @@ +# ==================== Stage 1: Builder (init + configure + build) ==================== +FROM ubuntu:24.04 AS builder + +ARG DEBIAN_FRONTEND=noninteractive +ARG CMAKE_VERSION=3.31.1 +ARG GCC_VERSION=14 +ARG RUST_VERSION=stable + +ENV DEBIAN_FRONTEND=${DEBIAN_FRONTEND} +ENV CMAKE_VERSION=${CMAKE_VERSION} +ENV GCC_VERSION=${GCC_VERSION} +ENV RUST_VERSION=${RUST_VERSION} +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 + +ENV PROJECT=/qlean-mini +ENV VENV=${PROJECT}/.venv +ENV BUILD=${PROJECT}/.build +ENV PATH=${VENV}/bin:/root/.cargo/bin:${PATH} +ENV CARGO_HOME=/root/.cargo +ENV RUSTUP_HOME=/root/.rustup + +WORKDIR ${PROJECT} + +# Copy project files +COPY . ${PROJECT} + +# Run all inits and build with vcpkg cache +RUN set -eux; \ + chmod +x .ci/scripts/*.sh; \ + # System dependencies and Rust via init.sh + ./.ci/scripts/init.sh; \ + # Clean up any existing venv that might have incompatible Python version + rm -rf ${VENV}; \ + # Python venv and cmake via init_py + make init_py + +# Configure and build with full vcpkg cache +RUN --mount=type=cache,target=/qlean-mini/.vcpkg,id=vcpkg-full \ + set -eux; \ + export PATH="${HOME}/.cargo/bin:${PATH}"; \ + source ${HOME}/.cargo/env 2>/dev/null || true; \ + # Init vcpkg inside cache mount if needed + if [ ! -f "/qlean-mini/.vcpkg/vcpkg" ]; then \ + make init_vcpkg; \ + fi; \ + # Clean build directory to avoid CMake cache path mismatch + rm -rf ${BUILD}; \ + make configure; \ + make build; \ + # Collect artifacts + mkdir -p /opt/artifacts/bin /opt/artifacts/modules /opt/artifacts/lib /opt/artifacts/vcpkg; \ + # Copy executable + cp -v ${BUILD}/src/executable/qlean /opt/artifacts/bin/; \ + # Copy all module .so files + find ${BUILD}/src/modules -type f -name "*_module.so" -exec cp -v {} /opt/artifacts/modules/ \; || true; \ + # Copy all other project .so libraries (app, utils, etc) + find ${BUILD}/src -type f -name "*.so" ! -name "*_module.so" -exec cp -v {} /opt/artifacts/lib/ \; || true; \ + # Copy vcpkg installed libraries + if [ -d "${BUILD}/vcpkg_installed" ]; then \ + cp -R ${BUILD}/vcpkg_installed /opt/artifacts/vcpkg/installed; \ + fi; \ + # List collected artifacts + echo "=== Collected artifacts ==="; \ + ls -lh /opt/artifacts/bin/; \ + ls -lh /opt/artifacts/modules/ || true; \ + ls -lh /opt/artifacts/lib/ || true + +# ==================== Stage 2: Runtime ==================== +FROM ubuntu:24.04 AS runtime + +# Install minimal runtime dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libstdc++6 \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Environment variables for runtime +ENV LD_LIBRARY_PATH=/opt/qlean/lib:/opt/qlean/vcpkg/installed/x64-linux/lib:/opt/qlean/vcpkg/installed/x64-linux-dynamic/lib:/opt/qlean/vcpkg/installed/lib:/usr/local/lib +ENV QLEAN_MODULES_DIR=/opt/qlean/modules + +WORKDIR /work + +# Copy artifacts from builder +COPY --from=builder /opt/artifacts/bin/qlean /usr/local/bin/qlean +COPY --from=builder /opt/artifacts/lib/ /opt/qlean/lib/ +COPY --from=builder /opt/artifacts/modules/ /opt/qlean/modules/ +COPY --from=builder /opt/artifacts/vcpkg/installed/ /opt/qlean/vcpkg/installed/ + +# Verify artifacts +RUN echo "=== Runtime image contents ===" && \ + ls -lh /usr/local/bin/qlean && \ + echo "=== Project libraries ===" && \ + ls -lh /opt/qlean/lib/ || true && \ + echo "=== Modules ===" && \ + ls -lh /opt/qlean/modules/ || true + +ENTRYPOINT ["qlean", "--modules-dir", "/opt/qlean/modules"] +CMD ["--help"] diff --git a/Dockerfile.runtime b/Dockerfile.runtime new file mode 100644 index 0000000..bd3ecda --- /dev/null +++ b/Dockerfile.runtime @@ -0,0 +1,43 @@ +# Use existing builder image +FROM qlean-mini:latest-builder AS builder + +# ==================== Stage 2: Runtime ==================== +FROM ubuntu:24.04 AS runtime + +# Install minimal runtime dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libstdc++6 \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Environment variables for runtime +ENV LD_LIBRARY_PATH=/opt/qlean/lib:/opt/qlean/vcpkg/installed/x64-linux/lib:/opt/qlean/vcpkg/installed/x64-linux-dynamic/lib:/opt/qlean/vcpkg/installed/lib:/usr/local/lib +ENV QLEAN_MODULES_DIR=/opt/qlean/modules + +WORKDIR /work + +# Copy binary +COPY --from=builder /qlean-mini/.build/src/executable/qlean /usr/local/bin/qlean +# Copy all project .so libraries +COPY --from=builder /qlean-mini/.build/src/app/*.so /opt/qlean/lib/ +COPY --from=builder /qlean-mini/.build/src/utils/*.so /opt/qlean/lib/ +# Copy modules +COPY --from=builder /qlean-mini/.build/src/modules/example/libexample_module.so /opt/qlean/modules/ +COPY --from=builder /qlean-mini/.build/src/modules/networking/libnetworking_module.so /opt/qlean/modules/ +COPY --from=builder /qlean-mini/.build/src/modules/production/libproduction_module.so /opt/qlean/modules/ +COPY --from=builder /qlean-mini/.build/src/modules/synchronizer/libsynchronizer_module.so /opt/qlean/modules/ +# Copy vcpkg libraries +COPY --from=builder /qlean-mini/.build/vcpkg_installed/ /opt/qlean/vcpkg/installed/ + +# Verify artifacts +RUN echo "=== Runtime image contents ===" && \ + ls -lh /usr/local/bin/qlean && \ + echo "=== Project libraries ===" && \ + ls -lh /opt/qlean/lib/ || true && \ + echo "=== Modules ===" && \ + ls -lh /opt/qlean/modules/ || true + +ENTRYPOINT ["qlean", "--modules-dir", "/opt/qlean/modules"] +CMD ["--help"] + diff --git a/Makefile b/Makefile index d795d33..07a771f 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,11 @@ endif OS_TYPE := $(shell bash -c 'source $(CI_DIR)/scripts/detect_os.sh && detect_os') +DOCKER_IMAGE ?= qlean-mini:latest +DOCKER_PLATFORM ?= linux/amd64 +DOCKER_REGISTRY ?= qdrvm +GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") + all: init_all configure build test @@ -38,11 +43,11 @@ configure: @echo "=== Configuring..." export PATH="$$HOME/.cargo/bin:$$PATH" && \ source $$HOME/.cargo/env 2>/dev/null || true && \ - VCPKG_ROOT=$(VCPKG) cmake --preset=default -DPython3_EXECUTABLE="$(VENV)/bin/python3" -B $(BUILD) $(PROJECT) + VCPKG_ROOT=$(VCPKG) cmake -G Ninja --preset=default -DPython3_EXECUTABLE="$(VENV)/bin/python3" -B $(BUILD) $(PROJECT) build: @echo "=== Building..." - cmake --build $(BUILD) + cmake --build $(BUILD) --parallel test: @echo "=== Testing..." @@ -52,4 +57,163 @@ clean_all: @echo "=== Cleaning..." rm -rf $(VENV) $(BUILD) $(VCPKG) -.PHONY: all init_all os init init_py init_vcpkg configure build test clean_all +# ==================== Docker Commands ==================== + +docker_build_builder: + @echo "=== [Stage 1/2] Building Docker BUILDER image (init + configure + build) ===" + @echo "=== Using build args from .ci/.env ===" + @echo " - CMAKE_VERSION=$(CMAKE_VERSION)" + @echo " - GCC_VERSION=$(GCC_VERSION)" + @echo " - RUST_VERSION=$(RUST_VERSION)" + @echo " - DEBIAN_FRONTEND=$(DEBIAN_FRONTEND)" + @echo "" + DOCKER_BUILDKIT=1 docker build \ + --build-arg CMAKE_VERSION=$(CMAKE_VERSION) \ + --build-arg GCC_VERSION=$(GCC_VERSION) \ + --build-arg RUST_VERSION=$(RUST_VERSION) \ + --build-arg DEBIAN_FRONTEND=$(DEBIAN_FRONTEND) \ + --target builder \ + --progress=plain \ + -t $(DOCKER_IMAGE)-builder . + @echo "" + @echo "✓ Builder image built: $(DOCKER_IMAGE)-builder" + +docker_build_runtime: + @echo "=== [Stage 2/2] Building Docker RUNTIME image (final) ===" + @echo "=== Using existing builder image: $(DOCKER_IMAGE)-builder ===" + @echo "" + DOCKER_BUILDKIT=1 docker build \ + -f Dockerfile.runtime \ + --progress=plain \ + -t $(DOCKER_IMAGE) . + @echo "" + @echo "✓ Runtime image built: $(DOCKER_IMAGE)" + +docker_build: docker_build_runtime + +docker_build_all: docker_build_builder docker_build_runtime + @echo "" + @echo "=== ✓ All Docker images built successfully ===" + @echo " - Builder: $(DOCKER_IMAGE)-builder" + @echo " - Runtime: $(DOCKER_IMAGE)" + +docker_run: + @echo "=== Running Docker image $(DOCKER_IMAGE) ===" + @echo "Note: --modules-dir is already set in ENTRYPOINT" + @echo "" + @echo "Usage examples:" + @echo " make docker_run # Show help" + @echo " make docker_run ARGS='--version' # Show version" + @echo " make docker_run ARGS='--base-path /work ...' # Run with custom args" + @echo "" + docker run --rm -it $(DOCKER_IMAGE) $(ARGS) + +docker_clean: + @echo "=== Cleaning Docker images ===" + docker rmi -f $(DOCKER_IMAGE) $(DOCKER_IMAGE)-builder 2>/dev/null || true + @echo "✓ Docker images cleaned" + +docker_inspect: + @echo "=== Docker images info ===" + @docker images | grep qlean-mini || echo "No qlean-mini images found" + +docker_verify: + @echo "=== Verifying Docker image: $(DOCKER_IMAGE) ===" + @echo "" + @echo "[1/6] Testing help command..." + @docker run --rm $(DOCKER_IMAGE) --help > /dev/null && echo " ✓ Help works" || (echo " ✗ Help failed" && exit 1) + @echo "" + @echo "[2/6] Testing version command..." + @docker run --rm $(DOCKER_IMAGE) --version && echo " ✓ Version works" || (echo " ✗ Version failed" && exit 1) + @echo "" + @echo "[3/6] Checking binary dependencies..." + @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\ + apt-get update -qq && apt-get install -y -qq file > /dev/null 2>&1 && \ + echo "Binary info:" && file /usr/local/bin/qlean && \ + echo "" && echo "Checking for missing libraries..." && \ + ldd /usr/local/bin/qlean | grep "not found" && exit 1 || echo " ✓ All binary dependencies OK"' + @echo "" + @echo "[4/6] Checking modules..." + @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\ + echo "Modules:" && ls -lh /opt/qlean/modules/ && \ + echo "" && echo "Checking module dependencies..." && \ + for mod in /opt/qlean/modules/*.so; do \ + echo "Checking $$(basename $$mod)..."; \ + ldd $$mod | grep "not found" && exit 1 || echo " ✓ OK"; \ + done' + @echo "" + @echo "[5/6] Checking environment variables..." + @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\ + echo "LD_LIBRARY_PATH=$$LD_LIBRARY_PATH" && \ + echo "QLEAN_MODULES_DIR=$$QLEAN_MODULES_DIR" && \ + echo "" && echo "Verifying paths exist:" && \ + ls -ld $$QLEAN_MODULES_DIR > /dev/null && echo " ✓ Modules dir exists" || (echo " ✗ Modules dir missing" && exit 1) && \ + ls -d /opt/qlean/lib > /dev/null && echo " ✓ Lib dir exists" || (echo " ✗ Lib dir missing" && exit 1)' + @echo "" + @echo "[6/6] Checking project libraries..." + @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\ + apt-get update -qq && apt-get install -y -qq file > /dev/null 2>&1 && \ + echo "Project libraries:" && ls /opt/qlean/lib/ && \ + echo "" && echo "Checking libapplication.so dependencies..." && \ + ldd /opt/qlean/lib/libapplication.so | grep "not found" && exit 1 || echo " ✓ All project libraries OK"' + @echo "" + @echo "=== ✓ All verification checks passed! ===" + +docker_verify_all: docker_verify + @echo "" + @echo "=== Verifying builder image: $(DOCKER_IMAGE)-builder ===" + @echo "" + @echo "Checking builder image exists..." + @docker image inspect $(DOCKER_IMAGE)-builder > /dev/null 2>&1 && echo " ✓ Builder image found" || (echo " ✗ Builder image not found" && exit 1) + @echo "Checking builder image size..." + @docker images $(DOCKER_IMAGE)-builder --format " Size: {{.Size}}" + @echo "" + @echo "=== ✓ All images verified! ===" + +docker_tag: + @echo "=== Tagging Docker images for push ===" + @echo "Registry: $(DOCKER_REGISTRY)" + @echo "Commit: $(GIT_COMMIT)" + @echo "" + docker tag $(DOCKER_IMAGE)-builder $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT) + docker tag $(DOCKER_IMAGE)-builder $(DOCKER_REGISTRY)/qlean-mini-builder:latest + docker tag $(DOCKER_IMAGE) $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT) + docker tag $(DOCKER_IMAGE) $(DOCKER_REGISTRY)/qlean-mini:latest + @echo "" + @echo "✓ Images tagged:" + @echo " - $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT)" + @echo " - $(DOCKER_REGISTRY)/qlean-mini-builder:latest" + @echo " - $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT)" + @echo " - $(DOCKER_REGISTRY)/qlean-mini:latest" + +docker_push: docker_tag + @echo "" + @echo "=== Pushing Docker images to $(DOCKER_REGISTRY) ===" + @echo "" + @echo "[1/4] Pushing builder with commit tag..." + docker push $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT) + @echo "" + @echo "[2/4] Pushing builder:latest..." + docker push $(DOCKER_REGISTRY)/qlean-mini-builder:latest + @echo "" + @echo "[3/4] Pushing runtime with commit tag..." + docker push $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT) + @echo "" + @echo "[4/4] Pushing runtime:latest..." + docker push $(DOCKER_REGISTRY)/qlean-mini:latest + @echo "" + @echo "✓ All images pushed successfully!" + +docker_build_push: docker_build_all docker_push + @echo "" + @echo "=== ✓ Build and push completed ===" + @echo "Images available at:" + @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini:latest" + @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT)" + @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini-builder:latest" + @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT)" + +.PHONY: all init_all os init init_py init_vcpkg configure build test clean_all \ + docker_build_builder docker_build_runtime docker_build docker_build_all \ + docker_run docker_clean docker_inspect docker_verify docker_verify_all \ + docker_tag docker_push docker_build_push diff --git a/example/0-smoke/genesis/config.yaml b/example/0-smoke/genesis/config.yaml new file mode 100644 index 0000000..5cfae3b --- /dev/null +++ b/example/0-smoke/genesis/config.yaml @@ -0,0 +1,5 @@ +# Genesis Settings +GENESIS_TIME: 1759469684 + +# Validator Settings +VALIDATOR_COUNT: 4 diff --git a/example/0-smoke/genesis/nodes.yaml b/example/0-smoke/genesis/nodes.yaml new file mode 100644 index 0000000..5c4a8b5 --- /dev/null +++ b/example/0-smoke/genesis/nodes.yaml @@ -0,0 +1,4 @@ +- enr:-IW4QNVvHz0m0tyHur-rUg88u20O7T-b_ZlJo_tFAtKXYIuQSD7RbE1ihpmf5gGzjvE7FqVAW2pYZhUsikiskVakTHYBgmlkgnY0gmlwhH8AAAGEcXVpY4IjKIlzZWNwMjU2azGhAmVoFLEuozqtJ7uxbf6RlL8ow-UlDKSYLzxKcbpZ13Zg +- enr:-IW4QMa4Ldb_hgv2P6Py91wQQ_7aIRnPSMyGnjM-yXocWe8VUo8yYyh43XBoEx549dYxzxfuwSrQhP7vVWrHsdiI_kABgmlkgnY0gmlwhH8AAAGEcXVpY4IjKYlzZWNwMjU2azGhA8PFJzjZs3Nmzn34yVzbnN5Mo5RhzwiWDxLnmoW1U7AV +- enr:-IW4QB0ZNaL8jHw14Gzb43ReLLwCGsLdWsxGHSKuGptmSC98NE6HwUco-8-o4Jb_VJpNqX8bgFS0eQveJ6KNNWyjKUUBgmlkgnY0gmlwhH8AAAGEcXVpY4IjKolzZWNwMjU2azGhAsQeX5os8a2pG4v2cGuMMXZYY2B-yzYLcZM3yEHa3_kW +- enr:-IW4QKG5cm3dR-7nNlHlEZnvOIDLQv_Hrqi4HDZqZOcOqnW4VF2eE_loEABlC-9iUB5wW-D-fq-kN-sdjQkxw6FfiOUBgmlkgnY0gmlwhH8AAAGEcXVpY4IjK4lzZWNwMjU2azGhAhXhXn0CrP2llJ7PNcWcimcUJ31GeGVMfk0MF2lnH4Ri diff --git a/example/0-smoke/genesis/validators.yaml b/example/0-smoke/genesis/validators.yaml new file mode 100644 index 0000000..54a1b4d --- /dev/null +++ b/example/0-smoke/genesis/validators.yaml @@ -0,0 +1,8 @@ +node_0: + - 0 +node_1: + - 1 +node_2: + - 2 +node_3: + - 3 diff --git a/example/config.yaml b/example/config.yaml index 1b88755..1cc62c9 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -2,7 +2,6 @@ general: name: NameFromConfig base_path: /tmp/lean_eth_node modules_dir: modules - spec_file: jamduna-spec.json database: directory: db @@ -24,7 +23,7 @@ logging: groups: - name: main sink: console - level: trace + level: info is_fallback: true children: - name: lean diff --git a/example/jamduna-spec.json b/example/jamduna-spec.json deleted file mode 100644 index cf7c2ab..0000000 --- a/example/jamduna-spec.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "bootnodes": [ - "e6srwvvha36ldoqzzcufiq7fevkxlqub6y2ll5wyzgnaeanvrwiha@127.0.0.1:40000", - "e5rxuvijvsfmre4dnolttxyjfa4yncnfcm2r7dkuxsnjfzzm7gdub@127.0.0.1:40001", - "ek5kkz37kij5alih6gmvustxi2vafc6pb3cvnvknj6pum4echyhpb@127.0.0.1:40002", - "e7lyaziunridjg5athudtcg5zclntartcjjgddvc377fjgmido2ma@127.0.0.1:40003", - "eikzzpoh4oycigw3jlhut5qqd3dl7styzucpn237opt55pulnt5yb@127.0.0.1:40004", - "erx3gfjrqky6per3avfs7jl5bgjegimvkpwl4g5g2mnsfffiozkha@127.0.0.1:40005" - ], - "id": "dev", - "genesis_header": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da293d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e948e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260fafff16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea020000ffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "genesis_state": { - "00b000040000000040641f50717f10e73c8fd443128f04e3abe2f4e0a94f2b": "0100000000", - "00b60011000000006dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf0": "0100000000", - "00fe00ff00ff00ff571677cde4d330fd3cd1d6d32ecbab502c92924433297a": "09626f6f74737472617042010008000011000000016e756c6c5f617574686f72697a6572206761732063616c6c20206761735f726573756c743a2000000000010000000000190000000000000019000100000000000d000000000000001e0000000000000018000000000000000800000000000000200000000000000026000000000000002e00000000000000307830303031303230333034303530363037303830393130313131323133313431353136313731383139323032313232323332343235323632373238323933303331333233333334333533363337333833393430343134323433343434353436343734383439353035313532353335343535353635373538353936303631363236333634363536363637363836393730373137323733373437353736373737383739383038313832383338343835383638373838383939303931393239333934393539363937393839394b598638d6c56d345310000027028e390300bc00c7007301e201fa0169028102f3020b039a033c046005320656068c06b606db06e2060c07c207e807ef07a408c7098a0ade0aff0a420b6e0b830bac0bc40b1e0cd30c6c0dcb0dda0d1e0e287e01951178ff7b108000648280883484891052092884882052085281778979859801977a2098aa209a77da9a0764298210800095118800287008817a3308810095177f33035701648b8bac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce22830817a3308810095177f33033701648b8bac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce295b7fe562780002a9a8c8100641bc87b0b3309780001330801330a0264275010041e098210800095118800320000951178ff7b108000648280883484891052091b84882052084582773308016429821080009511880028b907827a3308810095177f33035701648b98ac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce22830827a3308810095177f33033701648b98ac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce295b7fe562780002a9a8c8100641bc87b0b3309780001330801330a02642750100867088210800095118800320000951150ff7b10a8007b15a0007b169800491118fffffe4811240a7b17289517247b17783307027b17107b1780009517287b1788003307067b17087b17900033072800017b177b1748491150024911689518787b18584911600295173095184850100ab103821538821b4082163033070233083309645a33000c0a6401821718c8670753370000ff2533071000039577000001018278c98709ab590fc9680901827aab8af27b7901481124010a7b17289517247b17788217107b1780009517287b1788008217087b17900082177b1748491150024911689518787b18584911600295173095184850100e2a03821538821b4082163033070233083309645a3300100a6401821718c8670753370000ff2533071000039577000001018278c98709ab590fc9680901827aab8af27b79013305027a15240a7b17289517247b17783307027b1780009517287b1788003307067b17900033072800017b17487b15504911689517787b17587b1560951730951848501012a002821538821b4082163033070233083309645a3300140a6401821718c8670753370000ff2733071000039577000001018278c98709ab5911c9680901827aab8af27b79330b01330700000133088210a8008215a000821698009511b00032009511b87b10407b15387b1630c88909ac895264758277977801330a08e4a909e498068568ff98883f5107128259087b1918491120017b17282806491120016417951a18646950101632821882170851081414080100000000000080aa870d520718007b57087b56018210408215388216309511483200009511c87b10307b15287b16206495647251087982a70851077c82a9105109aa0082a833061000033303000001c8360601826ac85a07aca7cf00957b0188bc01d8b30bd4cb0b520bbf00826bababe57b67330c7b1218330a100003330b0000017b1ac8ba0ac97a077b17107b18086496501018bb035436000001980082121882181033077b280828b50049120833070128ac0033091000033307000001c8790901829ac85a08aca85f958b0188bc01d8b70bd4cb0b520b50829bababe77b98330c01ababdf283133091000033307000001c8790901829ac85a08aca82b958b0188bc01d8b70bd4cb0b520b1c829bababe77b98330c013307100004c9870833077b280828393307017b27082831646c82179577000001821218821608018278c98709ab6911c9c80901827aab8af27b79330b0182181033077b2808017b25107b2782103082152882162095113832009511b87b10407b15387b163095880151085364758277977901330a08e4a808e489068568ff98883f5107128259087b1918491120017b17282806491120016417951a18646950101a6dfe821882170851081414080100000000000080aa870d520718007b57087b56018210408215388216309511483200009511c87b10307b15287b1620828a086475510a3582893307959b080182bcc8c70795aaff95bb10520af5828a18510a248299088e99887a1085aa01d4a909897ae0a90952090a3307330901284a977701015107405707736452330b1000033309000001c89b0b0182bcc87c0aacca5995a501885601d85905d4650552054a82b6abc6e77bba3309100004c9a90964252806330901017b177b1908491110641750101c290652072282171082180882197b57107b58087b598210308215288216209511383200000000827233090000ff952afffffeac9a288277083309100003959900000101829ac9a90bab7b12c92a0b01829cabacf27b9b3308320032009511e07b10187b15107b16086475827a108277c9a7076496ac972a7b1a825708c8a707646950102241018217c876067b56103307821018821510821608951120320064577b1864a864695010247bfc8218825a1028c79511d87b10207b15187b16106486838833098000647aae982b82a81082a7ab781264a764a55010280ffe645a8258100182a708c8870778769588017ba81028ab008b670b48110c52071c9867068677c00078170c84673f8677800078170d33060228638b671052072998670c8677e00078170c97673498773a9577800078170d84673f8677800078170e330603283797672b98773d9577f00078170c97672e98773a9577800078170d97673498773a9577800078170e84673f8677800078170f3306040182a51082a7c95707ac672d7b1a82a708c8570795180c646950102a34c8560682177b7610013307821020821518821610951128320064a76458646964a550102c6efb645a82551028c42867049511e07b10187b15107b1608531910648d7a84a207c8270b510215648c6475017cca785a95550195cc01acb5f6c82803c929028424f8843807c84b0a51083b58044b973803848038843cf882c58d8884883895cc080182c6d00505cf8609d459097bb995bb0895cc086465acabeb281c647a520921282f58041364380182897bb995bb08958808acabf6c84308842907510914c8a909017c8b78ab95aa01958801ac9af68210188215108216089511203200827701289511a07b10587b15507b16487b19087b18987a04330927532a71028100330927951b463a080000037b1818491110f0d80033037b1433057a00013300ffe0f505016474821718d6740798770b821810c08702c8420a97a230982832ca3808988c119888108488fe0796cc9cc8ca0a97aa3198aa30c858087c8c7c8801c85a0a7ca6017caa78b8ff78bcfe78b60178ba9599fc95bbfcac40ac5417633b97783098883296887b14988811968a9cc8a7079777319877309599fe330a7a0001c8a7077c7a017c77951b21c89b0b78ba0178b764870155170a149599ff951821c89808837730788728219777019599fe33087a0001c887077c78017c77951a21c89a0a78a80178a701951b21c89b0b9a9c2733097800018217088218330a5010321282105882155082164895116032009511987b10607b15587b16507b1b1851084f807634846801330b2b3305000011db8b057b1528c8c8028468045108427b19303308510a1a64a5017d9b89bbc085bb01c8b8089555ff9599015205efc882027b1a3882787b1c20520823284181763495c2014911282d8468045208c44911307b1a3882787b1c20510824827508ae521e8466085206507c7938c9250a5a1901a1005209ae007b1a1028ad0082752082762864576468821928821a30821b385010346d013308015207bc00826c186457821818821920821060821558821650951168320c7b12408178307b18087c79387b19827920827628481730307b1710461738017b194864976468821928821a30821b385010361901520767821740c97505955501019555ff5105c600826920330830821748b49001385107ec28435219021298a90195a8019888017b1810280864a9491110018278207b18488276288177307b1740959501019555ff510527826920821748821840b490013a5107ed330801016487821060821558821650951168320082154864576468821928821a30821b3850103c89003308015207d8826a186457821818821920b4a0013e3308015207c38217109a777b17383305ff01821738c857075117ff42826920821748821840b49001409555015107e62837826a18821748821818821920b4a0014233080152078233088217108219087a79308219787938286fff821510821710d875082863ff821710d87508285aff9511d87b10207b15187b1610839c330200001164a66485647aaa2c20825c2064a764987b1b087b1ab4c00144821a821b08647833070152082051061a825c1864a7646864b9821020821518821610951128320c3307018210208215188216109511283200951178ff7b1080007b15787b1670491160203309037819684911304911408286207b175033074800017b17585106060182872851077301330982857b18088288107b18189578ff9788039888039588017b189555089678389566183307307b1710017b18207b19288259510917821a588217508258f882aa18b4a0014652074e018167107a17607c67187817688167147a17648268f8826751082652180116977704821818c88707827808821910aa98073308280f827782773308012806330801017b18307b17388268e88267f051082652180116977704821818c88707827808821910aa98073308280f827782773308012806330801017b18407b1748826708977704821818c878088287828908951830b49001485207b5009555108218209588c8956638821928959901520836ff2869828718510771828510330a977904c859097b19207b180882869577ff9777049877049577017b17956608017b1a288269510916821a588217508268f882aa18b4a0014a5207568257825908951830b490014c520747956610955510821a2895aa01821720ab75c7821808821a828708ac7a0d282c330a828708540725828797a804c88709821a58821750829882990882aa18b4a0014e51070833070128053307018210800082157882167095118800320089284952494a112992254952522a922549525212a284a410d1889224a58814c99224292915c99224292909514252886844449049924824114a2489242449952641084bb234264924122592441292a44a93208425599a4c9248244292244948922a4d82109664a9a188889224a924499244ca529404904c93a44952491285902592244a258492a410484a9284902549925253085992242935a49454215992a5a64c495292249524499248590a250124d3246992a4549624494a5252caa410b22449520aa92c29242929497a2124212cc9529524a5949242494952552825494549a950262549924822294924892492924812492289644a4a52a42c49aa0aa5244994a45692244992a49225292995d24a92242b4992d449929220090922044b9224491249929294a424914492242129498d24a92424256992908a2429499224214992d29224494a4a22494a922409912495242452922425494924524942924c24891449529924c92449244b92524942929248523289249194249194a42449922449a14a2a452949aa24a94952441249928444a2922449499249499288244952224992aa9449924892a42a6592242511499244494a9292a44c4992484924499294545292944452238900", - "00fe00ff00ff00ffca4b52ff29ea7c74d6bd301d1cae77ef7f24c8d294c847": "09626f6f747374726170000000000000010000000197040000060284067900300196010902a402e702280d00000028ae0000002803039511e07b10187b15107b1608648664783309043307000001ac967c9566fc5106769587047d7833050159083a8489ff003305025329c0002d3305035329e000253305045329f0001d3305055329f800153305065329fc000d8898fe009a850801ac564564587b17501002f4026478e45607c95707d88709e48707c98707887720d479098217c8750594983307000001da95072805330801821018821510821608951120320000951150ff7b10a8007b15a0007b169800330908ac985c013309fcaa973c0251073802958af8957508510a457d583306015908408489ff003306025329c0002d3306035329e000253306045329f0001d3306055329f800153306065329fc000d8898fe009a860801ae6a092805013306017b166457646864a65010043d02821a5107f000e4a607c9a707531760e600c85a089576a09587607b1751064c7d783305015908378489ff003305025329c0002d3305035329e000253305045329f0001d3305055329f800153305065329fc000d8898fe009a850801ac567f016458501006d70128073305330701e45608c95808e47808c97808330920ac98768219c89505c857079585e09577207b1751055e7d783306015908378489ff003306025329c0002d3306035329e000253306045329f0001d3306055329f800153306065329fc000d8898fe009a860801ac650d0164685010086401ae56198217c867077c7851082933083307289c0033065605ed33083307000001018210a8008215a000821698009511b0003200e46508c96808957501330701e47807330902957affae98093306330828517d573306015907378477ff003306025327c0002d3306035327e000253306045327f0001d3306055327f800153306065327fc000d8877fe009a7608017b1aac6a746457646850100ac9006478821a01c86507e46a09c96909e6890801c878088088fc330964330a640a0964757b1708481114951714330804951908330a040a0395171833098000330850100c33330820a107330964951a1864570a0b8117083d0700000233070000023308202824ff000000003307000001330832008d7a84aa07c8a70b510a0e647c0178c895cc01acbcfbc9a903843cf8c8cb0a580c1d8482ff0014090101010101010101ca920c017bbc95bb08acabfb843907520905280ec8a9090178a895aa01ac9afb320051089b0064797c77510791005127ff0090006c7a570a09330a330828735527c0000d330a01330b80284a5527e0000e330a02330b40ff283c5527f0000e330a03330b20ff282e5527f8000e330a04330b10ff28205527fc000e330a05330b08ff2812887afe00330b04ff93ab02ff85aa0701ae8a2b3308c8b70764ab01c8b90c7ccc97880895bbffd4c808520bf28aa903cf9707c88707320032000000002124492a21494a22212121212132154a9224a5909a248d88482422494924242424244426ad0a45129294924848484848888c4235499292a4944442424242426414524aa5144644942449529544424242424264a950936492549224258a905442483e54495a92241140962465495111942a24854421514814124544a6342549923a", - "01000000000000000000000000000000000000000000000000000000000000": "08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "02000000000000000000000000000000000000000000000000000000000000": "3cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e8", - "03000000000000000000000000000000000000000000000000000000000000": "00", - "04000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085f9095f4abd040839d793d89ab5ff25c61e50c844ab6765e2c0b22373b5a8f6fbe5fc0cd61fdde580b3d44fe1be127197e33b91960b10d2c6fc75aec03f36e16c2a8204961097dbc2c5ba7655543385399cc9ef08bf2e520ccf3b0a7569d88492e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf01aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bc5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d48e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e33d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d03d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d05e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161df16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d3d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d0aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bcaa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bc3d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e300", - "05000000000000000000000000000000000000000000000000000000000000": "00000000", - "06000000000000000000000000000000000000000000000000000000000000": "6f6ad2224d7d58aec6573c623ab110700eaca20a48dc2965d535e466d524af2a835ac82bfa2ce8390bb50680d4b7a73dfa2a4cff6d8c30694b24a605f9574eafd2d34655ebcad804c56d2fd5f932c575b6a5dbb3f5652c5202bcc75ab9c2cc958a715731759b7fceaae288bcb70a605c31cbdfae83d0f4a45dfc2b2458dc9fae", - "07000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf4260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "08000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf4260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "09000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf4260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "0a000000000000000000000000000000000000000000000000000000000000": "0000", - "0b000000000000000000000000000000000000000000000000000000000000": "00000000", - "0c000000000000000000000000000000000000000000000000000000000000": "00000000000000000000000000", - "0d000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "0e000000000000000000000000000000000000000000000000000000000000": "000000000000000000000000", - "0f000000000000000000000000000000000000000000000000000000000000": "000000000000000000000000", - "ff000000000000000000000000000000000000000000000000000000000000": "fcca4b52ff29ea7c74d6bd301d1cae77ef7f24c8d294c847bc82f93c5a70c26e00e40b540200000064000000000000006400000000000000081700000000000004000000" - } -} \ No newline at end of file diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index b103d97..9931f58 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -23,11 +23,16 @@ add_custom_command( VERBATIM ) add_library(build_version - ${CMAKE_BINARY_DIR}/generated/app/build_version.cpp + "${BUILD_VERSION_CPP}" ) +set(DEFAULT_LOGGING_YAML_CPP "${CMAKE_BINARY_DIR}/generated/app/default_logging_yaml.cpp") +file(READ "${CMAKE_SOURCE_DIR}/example/config.yaml" DEFAULT_LOGGING_YAML) +configure_file("${CMAKE_CURRENT_LIST_DIR}/default_logging_yaml.cpp.in" "${DEFAULT_LOGGING_YAML_CPP}") + add_library(app_configuration SHARED configuration.cpp + "${DEFAULT_LOGGING_YAML_CPP}" ) target_link_libraries(app_configuration Boost::boost @@ -39,9 +44,10 @@ add_library(app_configurator SHARED ) target_link_libraries(app_configurator app_configuration - yaml-cpp::yaml-cpp Boost::program_options build_version + p2p::libp2p + yaml-cpp::yaml-cpp ) add_library(app_state_manager SHARED @@ -52,14 +58,25 @@ target_link_libraries(app_state_manager logger ) +add_library(bootnodes SHARED + bootnodes.cpp +) +target_link_libraries(bootnodes + p2p::libp2p +) + add_library(chain_spec SHARED impl/chain_spec_impl.cpp ) target_link_libraries(chain_spec + bootnodes + enr logger Boost::property_tree app_configuration sszpp + p2p::libp2p + yaml-cpp::yaml-cpp ) add_library(application SHARED diff --git a/src/app/bootnodes.cpp b/src/app/bootnodes.cpp new file mode 100644 index 0000000..9f24e36 --- /dev/null +++ b/src/app/bootnodes.cpp @@ -0,0 +1,46 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/bootnodes.hpp" + +#include +#include + +// Implementation file for bootnodes - currently header-only but +// keeping this file for future implementation needs +namespace lean::app { + BootnodeInfo::BootnodeInfo(libp2p::multi::Multiaddress addr, + libp2p::PeerId id) + : address(std::move(addr)), peer_id(std::move(id)) {} + + Bootnodes::Bootnodes(std::vector nodes) + : nodes_(std::move(nodes)) {} + + const std::vector &Bootnodes::getBootnodes() const { + return nodes_; + } + + /** + * @brief Add a bootnode + */ + void Bootnodes::addBootnode(BootnodeInfo node) { + nodes_.emplace_back(std::move(node)); + } + + /** + * @brief Check if any bootnodes are configured + */ + bool Bootnodes::empty() const { + return nodes_.empty(); + } + + /** + * @brief Get the number of configured bootnodes + */ + size_t Bootnodes::size() const { + return nodes_.size(); + } +} // namespace lean::app diff --git a/src/app/bootnodes.hpp b/src/app/bootnodes.hpp new file mode 100644 index 0000000..d9240e7 --- /dev/null +++ b/src/app/bootnodes.hpp @@ -0,0 +1,59 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include + +namespace lean::app { + + /** + * @brief Represents a single bootnode with its address and peer ID + */ + struct BootnodeInfo { + libp2p::multi::Multiaddress address; + libp2p::PeerId peer_id; + + BootnodeInfo(libp2p::multi::Multiaddress addr, libp2p::PeerId id); + }; + + /** + * @brief Container for bootstrap nodes configuration + */ + class Bootnodes { + public: + Bootnodes() = default; + explicit Bootnodes(std::vector nodes); + + /** + * @brief Get all bootnode information + */ + const std::vector &getBootnodes() const; + + /** + * @brief Add a bootnode + */ + void addBootnode(BootnodeInfo node); + + /** + * @brief Check if any bootnodes are configured + */ + bool empty() const; + + /** + * @brief Get the number of configured bootnodes + */ + size_t size() const; + + private: + std::vector nodes_; + }; + +} // namespace lean::app diff --git a/src/app/chain_spec.hpp b/src/app/chain_spec.hpp index 63b277a..78c4b50 100644 --- a/src/app/chain_spec.hpp +++ b/src/app/chain_spec.hpp @@ -11,13 +11,7 @@ #include -#include "types/types.hpp" - -namespace lean { - - using NodeAddress = Stub; - -} +#include "app/bootnodes.hpp" namespace lean::app { @@ -25,14 +19,7 @@ namespace lean::app { public: virtual ~ChainSpec() = default; - [[nodiscard]] virtual const std::string &id() const = 0; - - [[nodiscard]] virtual const std::vector &bootNodes() const = 0; - - [[nodiscard]] virtual const qtils::ByteVec &genesisHeader() const = 0; - - [[nodiscard]] virtual const std::map & - genesisState() const = 0; + [[nodiscard]] virtual const app::Bootnodes &getBootnodes() const = 0; }; } // namespace lean::app diff --git a/src/app/configuration.cpp b/src/app/configuration.cpp index e18e794..02a2834 100644 --- a/src/app/configuration.cpp +++ b/src/app/configuration.cpp @@ -27,18 +27,39 @@ namespace lean::app { return name_; } - const std::filesystem::path &Configuration::basePath() const { - return base_path_; + const std::string &Configuration::nodeId() const { + return node_id_; } - const std::filesystem::path &Configuration::specFile() const { - return spec_file_; + const std::filesystem::path &Configuration::basePath() const { + return base_path_; } const std::filesystem::path &Configuration::modulesDir() const { return modules_dir_; } + const std::filesystem::path &Configuration::bootnodesFile() const { + return bootnodes_file_; + } + + const std::filesystem::path &Configuration::validatorRegistryPath() const { + return validator_registry_path_; + } + + const std::filesystem::path &Configuration::genesisConfigPath() const { + return genesis_config_path_; + } + + const std::optional &Configuration::listenMultiaddr() + const { + return listen_multiaddr_; + } + + const libp2p::crypto::KeyPair &Configuration::nodeKey() const { + return node_key_.value(); + } + const Configuration::DatabaseConfig &Configuration::database() const { return database_; } diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index b6e9e11..faf49d7 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -6,9 +6,12 @@ #pragma once #include +#include #include #include +#include +#include #include namespace lean::app { @@ -34,9 +37,17 @@ namespace lean::app { [[nodiscard]] virtual const std::string &nodeVersion() const; [[nodiscard]] virtual const std::string &nodeName() const; + [[nodiscard]] virtual const std::string &nodeId() const; [[nodiscard]] virtual const std::filesystem::path &basePath() const; - [[nodiscard]] virtual const std::filesystem::path &specFile() const; [[nodiscard]] virtual const std::filesystem::path &modulesDir() const; + [[nodiscard]] virtual const std::filesystem::path &bootnodesFile() const; + [[nodiscard]] virtual const std::filesystem::path &validatorRegistryPath() + const; + [[nodiscard]] virtual const std::filesystem::path &genesisConfigPath() + const; + [[nodiscard]] virtual const std::optional & + listenMultiaddr() const; + [[nodiscard]] virtual const libp2p::crypto::KeyPair &nodeKey() const; [[nodiscard]] virtual const DatabaseConfig &database() const; @@ -47,9 +58,14 @@ namespace lean::app { std::string version_; std::string name_; + std::string node_id_; std::filesystem::path base_path_; - std::filesystem::path spec_file_; std::filesystem::path modules_dir_; + std::filesystem::path bootnodes_file_; + std::filesystem::path validator_registry_path_; + std::filesystem::path genesis_config_path_; + std::optional listen_multiaddr_; + std::optional node_key_; DatabaseConfig database_; MetricsConfig metrics_; diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index 8ef89a3..340e9ee 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,9 @@ #include "app/build_version.hpp" #include "app/configuration.hpp" +#include "app/default_logging_yaml.hpp" +#include "log/formatters/filepath.hpp" +#include "modules/networking/get_node_key.hpp" #include "utils/parsers.hpp" using Endpoint = boost::asio::ip::tcp::endpoint; @@ -100,11 +104,19 @@ namespace lean::app { general_options.add_options() ("help,h", "Show this help message.") ("version,v", "Show version information.") - ("base_path", po::value(), "Set base path. All relative paths will be resolved based on this path.") + ("base-path", po::value(), "Set base path. All relative paths will be resolved based on this path.") + ("data-dir", po::value(), "Alias for \"--base-path\".") ("config,c", po::value(), "Optional. Filepath to load configuration from. Overrides default configuration values.") - ("spec_file", po::value(), "Set path to spec file.") - ("modules_dir", po::value(), "Set path to directory containing modules.") + ("genesis", po::value(), "Set path to genesis config yaml file (genesis/config.yaml).") + ("listen-addr", po::value(), "Set libp2p listen multiaddress.") + ("modules-dir", po::value(), "Set path to directory containing modules.") + ("bootnodes", po::value(), "Set path to yaml file containing boot node ENRs (genesis/nodes.yaml).") + ("validator-registry-path", + po::value(), + "Set path to yaml file containing validator registry (genesis/validators.yaml).") ("name,n", po::value(), "Set name of node.") + ("node-id", po::value(), "Node id from validator registry (genesis/validators.yaml).") + ("node-key", po::value(), "Set secp256k1 node key as hex string (with or without 0x prefix).") ("log,l", po::value>(), "Sets a custom logging filter.\n" "Syntax: =, e.g., -llibp2p=off.\n" @@ -211,13 +223,25 @@ namespace lean::app { } outcome::result Configurator::getLoggingConfig() { + auto load_default = [&]() -> outcome::result { + try { + return YAML::Load(defaultLoggingYaml())["logging"]; + } catch (const std::exception &e) { + file_errors_ << "E: Failed to load default logging config: " << e.what() + << "\n"; + return Error::ConfigFileParseFailed; + } + }; + + if (not config_file_.has_value()) { + return load_default(); + } auto logging = (*config_file_)["logging"]; if (logging.IsDefined()) { return logging; } - return YAML::Node{}; // TODO return default logging config + return load_default(); } - outcome::result> Configurator::calculateConfig( qtils::SharedRef logger) { logger_ = std::move(logger); @@ -244,27 +268,56 @@ namespace lean::app { file_has_error_ = true; } } - auto base_path = section["base_path"]; + auto node_id = section["node-id"]; + if (node_id.IsDefined()) { + if (node_id.IsScalar()) { + auto value = node_id.as(); + config_->node_id_ = value; + } else { + file_errors_ << "E: Value 'general.node_id' must be scalar\n"; + file_has_error_ = true; + } + } + auto node_key = section["node-key"]; + if (node_key.IsDefined()) { + if (node_key.IsScalar()) { + auto value = node_key.as(); + boost::trim(value); + if (auto r = keyPairFromPrivateKeyHex(value)) { + config_->node_key_ = r.value(); + } else { + fmt::println(file_errors_, + "E: Value 'general.node_key' must be private key " + "hex or it's file path: {}", + r.error()); + file_has_error_ = true; + } + } else { + file_errors_ << "E: Value 'general.node_key' must be scalar\n"; + file_has_error_ = true; + } + } + auto base_path = section["base-path"]; if (base_path.IsDefined()) { if (base_path.IsScalar()) { auto value = base_path.as(); config_->base_path_ = value; } else { - file_errors_ << "E: Value 'general.base_path' must be scalar\n"; + file_errors_ << "E: Value 'general.base-path' must be scalar\n"; file_has_error_ = true; } } - auto spec_file = section["spec_file"]; - if (spec_file.IsDefined()) { - if (spec_file.IsScalar()) { - auto value = spec_file.as(); - config_->spec_file_ = value; + auto genesis = section["genesis"]; + if (genesis.IsDefined()) { + if (genesis.IsScalar()) { + auto value = genesis.as(); + config_->genesis_config_path_ = value; } else { - file_errors_ << "E: Value 'general.spec_file' must be scalar\n"; + file_errors_ << "E: Value 'general.genesis' must be scalar\n"; file_has_error_ = true; } } - auto modules_dir = section["modules_dir"]; + auto modules_dir = section["modules-dir"]; if (modules_dir.IsDefined()) { if (modules_dir.IsScalar()) { auto value = modules_dir.as(); @@ -274,6 +327,46 @@ namespace lean::app { file_has_error_ = true; } } + auto listen_addr = section["listen-addr"]; + if (listen_addr.IsDefined()) { + if (listen_addr.IsScalar()) { + auto value = listen_addr.as(); + boost::trim(value); + if (auto r = libp2p::Multiaddress::create(value)) { + config_->listen_multiaddr_ = r.value(); + } else { + fmt::println(file_errors_, + "E: Value 'general.listen_addr' must be valid " + "multiaddress: {}", + r.error()); + file_has_error_ = true; + } + } else { + file_errors_ << "E: Value 'general.listen_addr' must be scalar\n"; + file_has_error_ = true; + } + } + auto bootnodes_file = section["bootnodes"]; + if (bootnodes_file.IsDefined()) { + if (bootnodes_file.IsScalar()) { + auto value = bootnodes_file.as(); + config_->bootnodes_file_ = value; + } else { + file_errors_ << "E: Value 'general.bootnodes' must be scalar\n"; + file_has_error_ = true; + } + } + auto validator_registry_path = section["validator-registry-path"]; + if (validator_registry_path.IsDefined()) { + if (validator_registry_path.IsScalar()) { + auto value = validator_registry_path.as(); + config_->validator_registry_path_ = value; + } else { + file_errors_ << "E: Value 'general.validator_registry_path' must " + "be scalar\n"; + file_has_error_ = true; + } + } } else { file_errors_ << "E: Section 'general' defined, but is not map\n"; file_has_error_ = true; @@ -305,55 +398,155 @@ namespace lean::app { config_->name_ = value; }); find_argument( - cli_values_map_, "base_path", [&](const std::string &value) { + cli_values_map_, "node-id", [&](const std::string &value) { + config_->node_id_ = value; + }); + find_argument( + cli_values_map_, "node-key", [&](std::string value) { + boost::trim(value); + if (auto r = keyPairFromPrivateKeyHex(value)) { + config_->node_key_ = r.value(); + } else { + SL_ERROR(logger_, + "'node-key' must be private key hex or it's file path: {}", + r.error()); + fail = true; + } + }); + find_argument( + cli_values_map_, "base-path", [&](const std::string &value) { config_->base_path_ = value; }); find_argument( - cli_values_map_, "modules_dir", [&](const std::string &value) { + cli_values_map_, "data-dir", [&](const std::string &value) { + config_->base_path_ = value; + }); + find_argument( + cli_values_map_, "modules-dir", [&](const std::string &value) { config_->modules_dir_ = value; }); find_argument( - cli_values_map_, "spec_file", [&](const std::string &value) { - config_->spec_file_ = value; + cli_values_map_, "listen-addr", [&](std::string value) { + boost::trim(value); + if (auto r = libp2p::Multiaddress::create(value)) { + config_->listen_multiaddr_ = r.value(); + } else { + SL_ERROR(logger_, + "'listen-addr' must be valid multiaddress: {}", + r.error()); + fail = true; + } + }); + find_argument( + cli_values_map_, "genesis", [&](const std::string &value) { + config_->genesis_config_path_ = value; }); + find_argument( + cli_values_map_, "bootnodes", [&](const std::string &value) { + config_->bootnodes_file_ = value; + }); + find_argument(cli_values_map_, + "validator-registry-path", + [&](const std::string &value) { + config_->validator_registry_path_ = value; + }); if (fail) { return Error::CliArgsParseFailed; } - // Check values - if (not config_->base_path_.is_absolute()) { - SL_ERROR(logger_, - "The 'base_path' must be defined as absolute: {}", - config_->base_path_.c_str()); - return Error::InvalidValue; + // Resolve base_path_ to an absolute path (relative to config file dir or + // CWD) + { + std::filesystem::path resolved = config_->base_path_; + if (not resolved.is_absolute()) { + if (auto cfg = find_argument(cli_values_map_, "config"); + cfg.has_value()) { + resolved = weakly_canonical(std::filesystem::path(*cfg).parent_path() + / resolved); + } else { + resolved = + weakly_canonical(std::filesystem::current_path() / resolved); + } + } else { + resolved = weakly_canonical(resolved); + } + config_->base_path_ = std::move(resolved); } + + // Validate base_path_ exists and is a directory if (not is_directory(config_->base_path_)) { SL_ERROR(logger_, "The 'base_path' does not exist or is not a directory: {}", - config_->base_path_.c_str()); + config_->base_path_); return Error::InvalidValue; } - current_path(config_->base_path_); - auto make_absolute = [&](const std::filesystem::path &path) { - return weakly_canonical(config_->base_path_.is_absolute() - ? path - : (config_->base_path_ / path)); + // Helper to resolve general paths: if provided via CLI -> relative to CWD, + // else if provided via config file -> relative to config file dir, + // else fallback to CWD. Always normalize. + auto resolve_relative = [&](const std::filesystem::path &path, + const char *cli_option_name) { + if (path.is_absolute()) { + return weakly_canonical(path); + } + // Was this option passed explicitly on CLI? + if (find_argument(cli_values_map_, cli_option_name) + .has_value()) { + return weakly_canonical(std::filesystem::current_path() / path); + } + // Otherwise, prefer resolving relative to config file location if present + if (auto cfg = find_argument(cli_values_map_, "config"); + cfg.has_value()) { + return weakly_canonical(std::filesystem::path(*cfg).parent_path() + / path); + } + // Fallback: current working directory + return weakly_canonical(std::filesystem::current_path() / path); }; - config_->modules_dir_ = make_absolute(config_->modules_dir_); + config_->modules_dir_ = + resolve_relative(config_->modules_dir_, "modules-dir"); if (not is_directory(config_->modules_dir_)) { SL_ERROR(logger_, "The 'modules_dir' does not exist or is not a directory: {}", - config_->modules_dir_.c_str()); + config_->modules_dir_); + return Error::InvalidValue; + } + + if (not config_->bootnodes_file_.empty()) { + config_->bootnodes_file_ = + resolve_relative(config_->bootnodes_file_, "bootnodes"); + if (not is_regular_file(config_->bootnodes_file_)) { + SL_ERROR(logger_, + "The 'bootnodes' file does not exist or is not a file: {}", + config_->bootnodes_file_); + return Error::InvalidValue; + } + } + + if (not config_->validator_registry_path_.empty()) { + config_->validator_registry_path_ = resolve_relative( + config_->validator_registry_path_, "validator-registry-path"); + if (not is_regular_file(config_->validator_registry_path_)) { + SL_ERROR( + logger_, + "The 'validator_registry_path' does not exist or is not a file: {}", + config_->validator_registry_path_); + return Error::InvalidValue; + } + } + + if (config_->genesis_config_path_.empty()) { + SL_ERROR(logger_, "The 'genesis' path must be provided"); return Error::InvalidValue; } - config_->spec_file_ = make_absolute(config_->spec_file_); - if (not is_regular_file(config_->spec_file_)) { + config_->genesis_config_path_ = + resolve_relative(config_->genesis_config_path_, "genesis"); + if (not is_regular_file(config_->genesis_config_path_)) { SL_ERROR(logger_, - "The 'spec_file' does not exist or is not a file: {}", - config_->spec_file_.c_str()); + "The 'genesis' file does not exist or is not a file: {}", + config_->genesis_config_path_); return Error::InvalidValue; } @@ -429,11 +622,10 @@ namespace lean::app { return Error::CliArgsParseFailed; } - // Check values + // Resolve database path against base_path_ when relative auto make_absolute = [&](const std::filesystem::path &path) { - return weakly_canonical(config_->base_path_.is_absolute() - ? path - : (config_->base_path_ / path)); + return weakly_canonical( + path.is_absolute() ? path : (config_->base_path_ / path)); }; config_->database_.directory = make_absolute(config_->database_.directory); @@ -568,6 +760,11 @@ namespace lean::app { config_->metrics_.enabled = false; } + if (not config_->node_key_.has_value()) { + config_->node_key_ = randomKeyPair(); + SL_INFO(logger_, "Generating random node key"); + } + return outcome::success(); } diff --git a/src/app/default_logging_yaml.cpp.in b/src/app/default_logging_yaml.cpp.in new file mode 100644 index 0000000..b3b9f4a --- /dev/null +++ b/src/app/default_logging_yaml.cpp.in @@ -0,0 +1,16 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/default_logging_yaml.hpp" + +namespace lean { + const std::string &defaultLoggingYaml() { + static std::string yaml = R"yaml( +@DEFAULT_LOGGING_YAML@ +)yaml"; + return yaml; + } +} // namespace lean diff --git a/src/app/default_logging_yaml.hpp b/src/app/default_logging_yaml.hpp new file mode 100644 index 0000000..ec97154 --- /dev/null +++ b/src/app/default_logging_yaml.hpp @@ -0,0 +1,16 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace lean { + /** + * YAML to read default logging config. + */ + const std::string &defaultLoggingYaml(); +} // namespace lean diff --git a/src/app/impl/chain_spec_impl.cpp b/src/app/impl/chain_spec_impl.cpp index 8660053..7d63472 100644 --- a/src/app/impl/chain_spec_impl.cpp +++ b/src/app/impl/chain_spec_impl.cpp @@ -11,8 +11,11 @@ #include #include +#include +#include -#include "types/block_header.hpp" +#include "modules/networking/read_nodes_yaml.hpp" +#include "serde/enr.hpp" OUTCOME_CPP_DEFINE_CATEGORY(lean::app, ChainSpecImpl::Error, e) { using E = lean::app::ChainSpecImpl::Error; @@ -31,62 +34,75 @@ OUTCOME_CPP_DEFINE_CATEGORY(lean::app, ChainSpecImpl::Error, e) { namespace lean::app { - namespace pt = boost::property_tree; - ChainSpecImpl::ChainSpecImpl(qtils::SharedRef logsys, qtils::SharedRef app_config) - : log_(logsys->getLogger("ChainSpec", "application")) { - if (auto res = loadFromJson(app_config->specFile()); res.has_error()) { - SL_CRITICAL(log_, "Can't init chain spec by json-file: {}", res.error()); + : log_(logsys->getLogger("ChainSpec", "application")), + app_config_(std::move(app_config)) { + if (auto res = load(); res.has_error()) { + SL_CRITICAL(log_, "Can't init chain spec: {}", res.error()); qtils::raise(res.error()); } } - outcome::result ChainSpecImpl::loadFromJson( - const std::filesystem::path &file_path) { - pt::ptree tree; - try { - pt::read_json(file_path, tree); - } catch (pt::json_parser_error &e) { - log_->error( - "Parser error: {}, line {}: {}", e.filename(), e.line(), e.message()); - return Error::PARSER_ERROR; - } - - OUTCOME_TRY(loadFields(tree)); - OUTCOME_TRY(loadGenesis(tree)); - OUTCOME_TRY(loadBootNodes(tree)); + outcome::result ChainSpecImpl::load() { + OUTCOME_TRY(loadBootNodes()); return outcome::success(); } - outcome::result ChainSpecImpl::loadFields( - const boost::property_tree::ptree &tree) { - OUTCOME_TRY(id, ensure("id", tree.get_child_optional("id"))); - id_ = id.get(""); + outcome::result ChainSpecImpl::loadBootNodes() { + // Check if bootnodes file is specified in configuration + if (app_config_->bootnodesFile().empty()) { + // No bootnodes file specified, return success with empty boot_nodes_ + SL_DEBUG(log_, + "No bootnodes file specified, using empty boot nodes list"); + return outcome::success(); + } - return outcome::success(); - } + auto parsed_res = readNodesYaml(app_config_->bootnodesFile()); + if (parsed_res.has_error()) { + SL_ERROR(log_, + "Failed to parse bootnodes YAML file {}: {}", + app_config_->bootnodesFile().string(), + parsed_res.error().message()); + return Error::PARSER_ERROR; + } - outcome::result ChainSpecImpl::loadGenesis( - const boost::property_tree::ptree &tree) { - // OUTCOME_TRY( - // genesis_header_hex, - // ensure("genesis_header", tree.get_child_optional("genesis_header"))); - // OUTCOME_TRY(genesis_header_encoded, - // qtils::ByteVec::fromHex(genesis_header_hex.data())); + auto parsed_nodes = std::move(parsed_res.value()); - BlockHeader header; - header.proposer_index = -1ull; - OUTCOME_TRY(genesis_header_encoded, encode(header)); + for (const auto &invalid_enr : parsed_nodes.invalid_entries) { + SL_WARN(log_, "Failed to decode ENR entry '{}'", invalid_enr); + } - genesis_header_ = std::move(genesis_header_encoded); - return outcome::success(); - } + std::vector bootnode_infos; + bootnode_infos.reserve(parsed_nodes.parsed.size()); + + for (const auto &entry : parsed_nodes.parsed) { + try { + auto peer_id = entry.enr.peerId(); + auto multiaddr = entry.enr.connectAddress(); + + bootnode_infos.emplace_back(std::move(multiaddr), std::move(peer_id)); + SL_INFO(log_, + "Added boot node: {} -> peer={}, address={}", + entry.raw, + bootnode_infos.back().peer_id, + bootnode_infos.back().address.getStringAddress()); + } catch (const std::exception &e) { + SL_WARN(log_, + "Failed to extract peer info from ENR '{}': {}", + entry.raw, + e.what()); + } + } + + bootnodes_ = app::Bootnodes(std::move(bootnode_infos)); + SL_INFO(log_, + "Loaded {} boot nodes from {} ({} invalid entries skipped)", + bootnodes_.size(), + app_config_->bootnodesFile().string(), + parsed_nodes.invalid_entries.size()); - outcome::result ChainSpecImpl::loadBootNodes( - const boost::property_tree::ptree &tree) { - // TODO Not implemented return outcome::success(); } diff --git a/src/app/impl/chain_spec_impl.hpp b/src/app/impl/chain_spec_impl.hpp index 02fd429..39e5e37 100644 --- a/src/app/impl/chain_spec_impl.hpp +++ b/src/app/impl/chain_spec_impl.hpp @@ -8,9 +8,9 @@ #include -#include #include +#include "app/bootnodes.hpp" #include "app/chain_spec.hpp" #include "log/logger.hpp" @@ -35,46 +35,17 @@ namespace lean::app { ChainSpecImpl(qtils::SharedRef logsys, qtils::SharedRef app_config); - const std::string &id() const override { - return id_; - } - - const std::vector &bootNodes() const override { - return boot_nodes_; - } - - const qtils::ByteVec &genesisHeader() const override { - return genesis_header_; - } - - const std::map &genesisState() - const override { - return genesis_state_; + const app::Bootnodes &getBootnodes() const override { + return bootnodes_; } private: - outcome::result loadFromJson(const std::filesystem::path &file_path); - outcome::result loadFields(const boost::property_tree::ptree &tree); - outcome::result loadGenesis(const boost::property_tree::ptree &tree); - outcome::result loadBootNodes( - const boost::property_tree::ptree &tree); - - template - outcome::result> ensure(std::string_view entry_name, - boost::optional opt_entry) { - if (not opt_entry) { - log_->error("Required '{}' entry not found in the chain spec", - entry_name); - return Error::MISSING_ENTRY; - } - return opt_entry.value(); - } + outcome::result load(); + outcome::result loadBootNodes(); log::Logger log_; - std::string id_; - std::vector boot_nodes_; - qtils::ByteVec genesis_header_; - std::map genesis_state_; + qtils::SharedRef app_config_; + app::Bootnodes bootnodes_; }; } // namespace lean::app diff --git a/src/app/impl/timeline_impl.cpp b/src/app/impl/timeline_impl.cpp index c640815..695689a 100644 --- a/src/app/impl/timeline_impl.cpp +++ b/src/app/impl/timeline_impl.cpp @@ -10,6 +10,7 @@ #include #include "app/state_manager.hpp" +#include "blockchain/genesis_config.hpp" #include "clock/clock.hpp" #include "log/logger.hpp" #include "modules/shared/prodution_types.tmp.hpp" @@ -25,7 +26,7 @@ namespace lean::app { qtils::SharedRef state_manager, qtils::SharedRef se_manager, qtils::SharedRef clock, - qtils::SharedRef config) + qtils::SharedRef config) : logger_(logsys->getLogger("Timeline", "application")), state_manager_(std::move(state_manager)), config_(std::move(config)), @@ -49,12 +50,12 @@ namespace lean::app { void TimelineImpl::start() { auto now = clock_->nowMsec(); - auto next_slot = now > config_->genesis_time // somehow now could be less - // than genesis time - ? (now - config_->genesis_time) / SLOT_DURATION_MS + 1 - : 1; - auto time_to_next_slot = - config_->genesis_time + SLOT_DURATION_MS * next_slot - now; + auto next_slot = + now > config_->config.genesis_time * 1000 + ? (now - config_->config.genesis_time * 1000) / SLOT_DURATION_MS + 1 + : 1; + auto time_to_next_slot = config_->config.genesis_time * 1000 + + SLOT_DURATION_MS * next_slot - now; if (time_to_next_slot < SLOT_DURATION_MS / 2) { ++next_slot; time_to_next_slot += SLOT_DURATION_MS; @@ -81,14 +82,15 @@ namespace lean::app { } auto now = clock_->nowMsec(); - auto next_slot = (now - config_->genesis_time) / SLOT_DURATION_MS + 1; - auto time_to_next_slot = - config_->genesis_time + SLOT_DURATION_MS * next_slot - now; + auto next_slot = + (now - config_->config.genesis_time * 1000) / SLOT_DURATION_MS + 1; + auto time_to_next_slot = config_->config.genesis_time * 1000 + + SLOT_DURATION_MS * next_slot - now; SL_INFO(logger_, "Next slot is {} in {}ms", next_slot, time_to_next_slot); const auto slot_start_abs = - config_->genesis_time + config_->config.genesis_time * 1000 + SLOT_DURATION_MS * msg->slot; // in milliseconds auto abs_interval1 = slot_start_abs + SECONDS_PER_INTERVAL * 1000; @@ -126,8 +128,8 @@ namespace lean::app { std::make_shared( 3, msg->slot, msg->epoch)); - const auto next_slot_abs = - config_->genesis_time + SLOT_DURATION_MS * (msg->slot + 1); + const auto next_slot_abs = config_->config.genesis_time * 1000 + + SLOT_DURATION_MS * (msg->slot + 1); auto time_to_next_slot_abs = ms_to_abs(next_slot_abs); se_manager_->notifyDelayed( std::chrono::milliseconds(time_to_next_slot_abs), diff --git a/src/app/impl/timeline_impl.hpp b/src/app/impl/timeline_impl.hpp index efb521e..37c844f 100644 --- a/src/app/impl/timeline_impl.hpp +++ b/src/app/impl/timeline_impl.hpp @@ -16,7 +16,7 @@ namespace lean::messages { struct SlotStarted; } namespace lean { - struct Config; + struct GenesisConfig; } namespace lean::log { class LoggingSystem; @@ -40,7 +40,7 @@ namespace lean::app { qtils::SharedRef state_manager, qtils::SharedRef se_manager, qtils::SharedRef clock, - qtils::SharedRef config); + qtils::SharedRef config); void prepare(); void start(); @@ -51,7 +51,7 @@ namespace lean::app { qtils::SharedRef logger_; qtils::SharedRef state_manager_; - qtils::SharedRef config_; + qtils::SharedRef config_; qtils::SharedRef clock_; qtils::SharedRef se_manager_; diff --git a/src/blockchain/CMakeLists.txt b/src/blockchain/CMakeLists.txt index cbf9ccb..ec2349f 100644 --- a/src/blockchain/CMakeLists.txt +++ b/src/blockchain/CMakeLists.txt @@ -4,21 +4,33 @@ # SPDX-License-Identifier: Apache-2.0 # +add_library(validator_registry + impl/validator_registry_impl.cpp +) +target_link_libraries(validator_registry + app_configuration + fmt::fmt + logger + yaml-cpp::yaml-cpp +) + add_library(blockchain fork_choice.cpp + genesis_config.cpp impl/block_storage_error.cpp impl/block_storage_impl.cpp impl/block_storage_initializer.cpp impl/block_tree_error.cpp impl/block_tree_impl.cpp - impl/fc_block_tree.cpp impl/block_tree_initializer.cpp impl/cached_tree.cpp - impl/genesis_block_header_impl.cpp + impl/fc_block_tree.cpp impl/storage_util.cpp state_transition_function.cpp ) target_link_libraries(blockchain Boost::boost + metrics sszpp + validator_registry ) diff --git a/src/blockchain/block_storage_error.hpp b/src/blockchain/block_storage_error.hpp index 8920c5b..02d2a8a 100644 --- a/src/blockchain/block_storage_error.hpp +++ b/src/blockchain/block_storage_error.hpp @@ -20,6 +20,7 @@ namespace lean::blockchain { GENESIS_BLOCK_NOT_FOUND, FINALIZED_BLOCK_NOT_FOUND, BLOCK_TREE_LEAVES_NOT_FOUND, + JUSTIFICATION_EMPTY }; } diff --git a/src/blockchain/fork_choice.cpp b/src/blockchain/fork_choice.cpp index 5ccea8c..7a31361 100644 --- a/src/blockchain/fork_choice.cpp +++ b/src/blockchain/fork_choice.cpp @@ -6,10 +6,12 @@ #include "blockchain/fork_choice.hpp" +#include #include +#include +#include "blockchain/genesis_config.hpp" #include "types/signed_block.hpp" -#include "utils/__debug_env.hpp" namespace lean { void ForkChoiceStore::updateSafeTarget() { @@ -67,6 +69,8 @@ namespace lean { } } } + + metric_latest_finalized_->set(latest_finalized_.slot); } } @@ -163,19 +167,12 @@ namespace lean { for (auto &signed_vote : latest_known_votes_ | std::views::values) { block.body.attestations.push_back(signed_vote); } - BOOST_OUTCOME_TRY( - auto state, - stf_.stateTransition({.message = block}, head_state, false)); + BOOST_OUTCOME_TRY(auto state, + stf_.stateTransition(block, head_state, false)); block.state_root = sszHash(state); block.setHash(); - // Store block and state in forkchoice store - auto block_hash = block.hash(); - blocks_.emplace(block_hash, block); - states_.emplace(block_hash, std::move(state)); - - // update head (not in spec) - head_ = block_hash; + BOOST_OUTCOME_TRY(onBlock(block)); return block; } @@ -183,12 +180,10 @@ namespace lean { outcome::result ForkChoiceStore::validateAttestation( const SignedVote &signed_vote) { - SL_INFO(logger_, - "Validating attestation for target {}@{}, source {}@{}", - signed_vote.data.target.slot, - signed_vote.data.target.root, - signed_vote.data.source.slot, - signed_vote.data.source.root); + SL_TRACE(logger_, + "Validating attestation for target {}, source {}", + signed_vote.data.target, + signed_vote.data.source); auto &vote = signed_vote.data; // Validate vote targets exist in store @@ -231,7 +226,7 @@ namespace lean { // Validate attestation structure and constraints BOOST_OUTCOME_TRY(validateAttestation(signed_vote)); - auto &validator_id = signed_vote.data.validator_id; + auto &validator_id = signed_vote.validator_id; auto &vote = signed_vote.data; if (is_from_block) { @@ -279,8 +274,8 @@ namespace lean { // chain if not available before adding block to forkchoice // Get post state from STF (State Transition Function) - auto state = - stf_.stateTransition({.message = block}, parent_state, true).value(); + BOOST_OUTCOME_TRY(auto state, + stf_.stateTransition(block, parent_state, true)); blocks_.emplace(block_hash, block); states_.emplace(block_hash, std::move(state)); @@ -297,7 +292,7 @@ namespace lean { std::vector> ForkChoiceStore::advanceTime(uint64_t now_sec) { - auto time_since_genesis = now_sec - config_.genesis_time / 1000; + auto time_since_genesis = now_sec - config_.genesis_time; std::vector> result{}; while (time_ < time_since_genesis) { @@ -314,7 +309,9 @@ namespace lean { current_slot, time_ * SECONDS_PER_INTERVAL); auto producer_index = current_slot % config_.num_validators; - auto is_producer = validator_index_ == producer_index; + auto is_producer = + validator_registry_->currentValidatorIndices().contains( + producer_index); if (is_producer) { acceptNewVotes(); @@ -326,15 +323,14 @@ namespace lean { res.error()); continue; } - const auto &new_block = res.value(); + auto &new_block = res.value(); - SignedBlock new_signed_block{.message = new_block, - .signature = qtils::ByteArr<32>{0}}; + auto new_signed_block = signBlock(std::move(new_block)); SL_INFO(logger_, - "Produced block for slot {} with parent {} state {}", - current_slot, - new_block.parent_root, + "Produced block {} with parent {} state {}", + new_signed_block.message.slotHash(), + new_signed_block.message.parent_root, new_signed_block.message.state_root); result.emplace_back(std::move(new_signed_block)); } @@ -342,40 +338,44 @@ namespace lean { // Interval one actions auto head_root = getHead(); auto head_slot = getBlockSlot(head_root); - BOOST_ASSERT_MSG(head_slot.has_value(), - "Head block must have a valid slot"); + if (not head_slot.has_value()) { + SL_ERROR(logger_, "Head block {} not found in store", head_root); + time_ += 1; + continue; + } Checkpoint head{.root = head_root, .slot = head_slot.value()}; auto target = getVoteTarget(); auto source = getLatestJustified(); SL_INFO(logger_, - "For slot {}: head is {}@{}, target is {}@{}, source is {}@{}", + "For slot {}: head is {}, target is {}, source is {}", current_slot, - head.root, - head.slot, - target.root, - target.slot, - source->root, - source->slot); - SignedVote signed_vote{.data = - Vote{ - .validator_id = validator_index_, - .slot = current_slot, - .head = head, - .target = target, - .source = *source, - }, - // signature with zero bytes for now - .signature = qtils::ByteArr<32>{0}}; - - // Dispatching send signed vote only broadcasts to other peers. Current - // peer should process attestation directly - auto res = processAttestation(signed_vote, false); - BOOST_ASSERT_MSG(res.has_value(), "Produced vote should be valid"); - SL_INFO(logger_, - "Produced vote for target {}@{}", - signed_vote.data.target.slot, - signed_vote.data.target.root); - result.emplace_back(std::move(signed_vote)); + head, + target, + source.value()); + for (auto validator_index : + validator_registry_->currentValidatorIndices()) { + auto signed_vote = signVote(validator_index, + Vote{ + .slot = current_slot, + .head = head, + .target = target, + .source = *source, + }); + + // Dispatching send signed vote only broadcasts to other peers. + // Current peer should process attestation directly + auto res = processAttestation(signed_vote, false); + if (not res.has_value()) { + SL_ERROR(logger_, + "Failed to process attestation for slot {}: {}", + current_slot, + res.error()); + continue; + } + SL_INFO( + logger_, "Produced vote for target {}", signed_vote.data.target); + result.emplace_back(std::move(signed_vote)); + } } else if (time_ % INTERVALS_PER_SLOT == 2) { // Interval two actions SL_INFO(logger_, @@ -459,20 +459,22 @@ namespace lean { } ForkChoiceStore::ForkChoiceStore( - const AnchorState &anchor_state, - const AnchorBlock &anchor_block, + const GenesisConfig &genesis_config, qtils::SharedRef clock, - qtils::SharedRef logging_system) - : validator_index_(getPeerIndex()), + qtils::SharedRef logging_system, + qtils::SharedRef validator_registry) + : validator_registry_(validator_registry), logger_( logging_system->getLogger("ForkChoiceStore", "fork_choice_store")) { + AnchorState anchor_state = STF::generateGenesisState(genesis_config.config); + AnchorBlock anchor_block = STF::genesisBlock(anchor_state); BOOST_ASSERT(anchor_block.state_root == sszHash(anchor_state)); anchor_block.setHash(); auto anchor_root = anchor_block.hash(); config_ = anchor_state.config; auto now_sec = clock->nowSec(); time_ = now_sec > config_.genesis_time - ? (now_sec - config_.genesis_time / 1000) / SECONDS_PER_INTERVAL + ? (now_sec - config_.genesis_time) / SECONDS_PER_INTERVAL : 0; head_ = anchor_root; safe_target_ = anchor_root; @@ -500,7 +502,8 @@ namespace lean { std::unordered_map states, Votes latest_known_votes, Votes latest_new_votes, - ValidatorIndex validator_index) + ValidatorIndex validator_index, + qtils::SharedRef validator_registry) : time_(now_sec / SECONDS_PER_INTERVAL), logger_( logging_system->getLogger("ForkChoiceStore", "fork_choice_store")), @@ -513,5 +516,5 @@ namespace lean { states_(std::move(states)), latest_known_votes_(std::move(latest_known_votes)), latest_new_votes_(std::move(latest_new_votes)), - validator_index_(validator_index) {} + validator_registry_(std::move(validator_registry)) {} } // namespace lean diff --git a/src/blockchain/fork_choice.hpp b/src/blockchain/fork_choice.hpp index 0ee51f5..e2a0a2d 100644 --- a/src/blockchain/fork_choice.hpp +++ b/src/blockchain/fork_choice.hpp @@ -6,20 +6,29 @@ #pragma once +#include #include #include #include #include +#include #include "blockchain/is_justifiable_slot.hpp" #include "blockchain/state_transition_function.hpp" +#include "blockchain/validator_registry.hpp" #include "clock/clock.hpp" +#include "metrics/histogram_timer.hpp" #include "types/block.hpp" #include "types/state.hpp" #include "types/validator_index.hpp" #include "utils/ceil_div.hpp" +namespace lean { + struct GenesisConfig; + struct SignedBlock; +} // namespace lean + namespace lean { class ForkChoiceStore { public: @@ -41,28 +50,29 @@ namespace lean { abort(); } - ForkChoiceStore(const AnchorState &anchor_state, - const AnchorBlock &anchor_block, + ForkChoiceStore(const GenesisConfig &genesis_config, qtils::SharedRef clock, - qtils::SharedRef logging_system); + qtils::SharedRef logging_system, + qtils::SharedRef validator_registry); - BOOST_DI_INJECT_TRAITS(const AnchorState &, - const AnchorBlock &, + BOOST_DI_INJECT_TRAITS(const GenesisConfig &, qtils::SharedRef, - qtils::SharedRef); + qtils::SharedRef, + qtils::SharedRef); // Test constructor - only for use in tests ForkChoiceStore(uint64_t now_sec, qtils::SharedRef logging_system, - Config config = {}, - BlockHash head = {}, - BlockHash safe_target = {}, - Checkpoint latest_justified = {}, - Checkpoint latest_finalized = {}, - Blocks blocks = {}, - std::unordered_map states = {}, - Votes latest_known_votes = {}, - Votes latest_new_votes = {}, - ValidatorIndex validator_index = 0); + Config config, + BlockHash head, + BlockHash safe_target, + Checkpoint latest_justified, + Checkpoint latest_finalized, + Blocks blocks, + std::unordered_map states, + Votes latest_known_votes, + Votes latest_new_votes, + ValidatorIndex validator_index, + qtils::SharedRef validator_registry); // Compute the latest block that the validator is allowed to choose as the // target @@ -167,8 +177,13 @@ namespace lean { std::unordered_map states_; Votes latest_known_votes_; Votes latest_new_votes_; - const ValidatorIndex validator_index_; + qtils::SharedRef validator_registry_; log::Logger logger_; + + metrics::GaugeHelper metric_latest_finalized_{ + "latest_finalized", + "", + }; }; BlockHash getForkChoiceHead(const ForkChoiceStore::Blocks &blocks, diff --git a/src/blockchain/genesis_config.cpp b/src/blockchain/genesis_config.cpp new file mode 100644 index 0000000..18ed56f --- /dev/null +++ b/src/blockchain/genesis_config.cpp @@ -0,0 +1,29 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "blockchain/genesis_config.hpp" + +#include +#include + +#include "app/configuration.hpp" +#include "log/logger.hpp" +#include "modules/production/read_config_yaml.hpp" + +namespace lean { + GenesisConfig::GenesisConfig(const log::LoggingSystem &logsys, + const app::Configuration &app_config) + : config{qtils::valueOrRaise( + readConfigYaml(app_config.genesisConfigPath()))} { + auto logger = logsys.getLogger("GenesisConfig", "genesis_config"); + SL_INFO(logger, + "Genesis config loaded: genesis_time={} (UTC {:%Y-%m-%d " + "%H:%M:%S}), num_validators={}", + config.genesis_time, + fmt::gmtime(config.genesis_time), + config.num_validators); + } +} // namespace lean diff --git a/src/blockchain/genesis_config.hpp b/src/blockchain/genesis_config.hpp new file mode 100644 index 0000000..5666f8d --- /dev/null +++ b/src/blockchain/genesis_config.hpp @@ -0,0 +1,29 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "types/config.hpp" + +namespace lean::app { + class Configuration; +} // namespace lean::app + + +namespace lean::log { + class LoggingSystem; +} // namespace lean::log + +namespace lean { + struct GenesisConfig { + GenesisConfig(const log::LoggingSystem &logsys, + const app::Configuration &app_config); + + Config config; + }; +} // namespace lean diff --git a/src/blockchain/impl/block_storage_error.cpp b/src/blockchain/impl/block_storage_error.cpp index 05508dd..778b7c0 100644 --- a/src/blockchain/impl/block_storage_error.cpp +++ b/src/blockchain/impl/block_storage_error.cpp @@ -23,6 +23,8 @@ OUTCOME_CPP_DEFINE_CATEGORY(lean::blockchain, BlockStorageError, e) { return "Genesis block not found"; case E::BLOCK_TREE_LEAVES_NOT_FOUND: return "Block tree leaves not found"; + case E::JUSTIFICATION_EMPTY: + return "Justification empty"; } return "Unknown error"; } diff --git a/src/blockchain/impl/block_storage_impl.cpp b/src/blockchain/impl/block_storage_impl.cpp index 74993a9..bed8764 100644 --- a/src/blockchain/impl/block_storage_impl.cpp +++ b/src/blockchain/impl/block_storage_impl.cpp @@ -219,7 +219,9 @@ namespace lean::blockchain { outcome::result BlockStorageImpl::putJustification( const Justification &justification, const BlockHash &hash) { - BOOST_ASSERT(not justification.empty()); + if(justification.empty()) { + return BlockStorageError::JUSTIFICATION_EMPTY; + } OUTCOME_TRY(encoded_justification, encode(justification)); OUTCOME_TRY(putToSpace(*storage_, diff --git a/src/blockchain/impl/fc_block_tree.cpp b/src/blockchain/impl/fc_block_tree.cpp index 7dcdefd..68b021b 100644 --- a/src/blockchain/impl/fc_block_tree.cpp +++ b/src/blockchain/impl/fc_block_tree.cpp @@ -105,7 +105,13 @@ namespace lean::blockchain { outcome::result> FCBlockTree::tryGetSignedBlock( const BlockHash block_hash) const { - throw std::runtime_error("FCBlockTree::tryGetSignedBlock()"); + auto &blocks = fork_choice_store_->getBlocks(); + auto it = blocks.find(block_hash); + if (it == blocks.end()) { + return std::nullopt; + } + // TODO(turuslan): signature + return SignedBlock{.message = it->second}; } void FCBlockTree::import(std::vector blocks) {} diff --git a/src/blockchain/impl/genesis_block_header_impl.cpp b/src/blockchain/impl/genesis_block_header_impl.cpp deleted file mode 100644 index 4d434a3..0000000 --- a/src/blockchain/impl/genesis_block_header_impl.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - - -#include "blockchain/impl/genesis_block_header_impl.hpp" - -#include "app/chain_spec.hpp" -#include "crypto/hasher.hpp" -#include "log/logger.hpp" -#include "sszpp/ssz++.hpp" - -namespace lean::blockchain { - - GenesisBlockHeaderImpl::GenesisBlockHeaderImpl( - const qtils::SharedRef &logsys, - const qtils::SharedRef &chain_spec, - const qtils::SharedRef &hasher) { - auto res = encode(chain_spec->genesisHeader()); - if (res.has_error()) { - auto logger = logsys->getLogger("ChainSpec", "application"); - SL_CRITICAL(logger, - "Failed to decode genesis block header from chain spec: {}", - res.error()); - qtils::raise_on_err(res); - } - updateHash(); - } - -} // namespace lean::blockchain diff --git a/src/blockchain/impl/genesis_block_header_impl.hpp b/src/blockchain/impl/genesis_block_header_impl.hpp deleted file mode 100644 index eaeb834..0000000 --- a/src/blockchain/impl/genesis_block_header_impl.hpp +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#include "blockchain/genesis_block_header.hpp" - -namespace lean::app { - class ChainSpec; -} -namespace lean::log { - class LoggingSystem; -} -namespace lean::crypto { - class Hasher; -} - -namespace lean::blockchain { - - class GenesisBlockHeaderImpl final : public GenesisBlockHeader { - public: - GenesisBlockHeaderImpl(const qtils::SharedRef &logsys, - const qtils::SharedRef &chain_spec, - const qtils::SharedRef &hasher); - }; - -} // namespace lean::blockchain diff --git a/src/blockchain/impl/validator_registry_impl.cpp b/src/blockchain/impl/validator_registry_impl.cpp new file mode 100644 index 0000000..63aa7a7 --- /dev/null +++ b/src/blockchain/impl/validator_registry_impl.cpp @@ -0,0 +1,165 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "blockchain/impl/validator_registry_impl.hpp" + +#include +#include + +#include +#include +#include + +#include "app/configuration.hpp" + +namespace lean { + enum class ValidatorRegistryError { + MAP_EXPECTED, + NODE_ID_SCALAR_EXPECTED, + INDICES_SCALAR_EXPECTED, + INVALID_VALUE_TYPE, + ALREADY_ASSIGNED, + }; + Q_ENUM_ERROR_CODE(ValidatorRegistryError) { + using E = decltype(e); + switch (e) { + case E::MAP_EXPECTED: + return "Validator registry YAML must be a YAML map"; + case E::NODE_ID_SCALAR_EXPECTED: + return "Validator registry YAML has non-scalar node id"; + case E::INDICES_SCALAR_EXPECTED: + return "Validator registry entry must contain scalar indices"; + case E::INVALID_VALUE_TYPE: + return "Validator registry entry has invalid value type"; + case E::ALREADY_ASSIGNED: + return "Validator index is already assigned to node"; + } + abort(); + } + + ValidatorRegistryImpl::ValidatorRegistryImpl( + qtils::SharedRef logging_system, + const app::Configuration &config) + : ValidatorRegistryImpl{std::move(logging_system), + YAML::LoadFile(config.validatorRegistryPath()), + config.nodeId()} {} + + ValidatorRegistryImpl::ValidatorRegistryImpl( + qtils::SharedRef logging_system, + std::string yaml, + std::string current_node_id) + : ValidatorRegistryImpl{std::move(logging_system), + YAML::Load(yaml), + std::move(current_node_id)} {} + + std::optional ValidatorRegistryImpl::nodeIdByIndex( + ValidatorIndex index) const { + if (auto it = index_to_node_.find(index); it != index_to_node_.end()) { + return it->second; + } + return std::nullopt; + } + + std::optional + ValidatorRegistryImpl::validatorIndicesForNodeId( + std::string_view node_id) const { + if (node_id.empty()) { + return std::nullopt; + } + if (auto it = node_to_indices_.find(std::string(node_id)); + it != node_to_indices_.end() and not it->second.empty()) { + return ValidatorIndices{it->second.begin(), it->second.end()}; + } + return std::nullopt; + } + + const ValidatorRegistry::ValidatorIndices & + ValidatorRegistryImpl::currentValidatorIndices() const { + return current_validator_indices_; + } + + ValidatorRegistryImpl::ValidatorRegistryImpl( + qtils::SharedRef logging_system, + YAML::Node root, + std::string current_node_id) + : logger_{logging_system->getLogger("ValidatorRegistry", + "validator_registry")}, + current_node_id_{std::move(current_node_id)} { + if (not root.IsDefined() or root.IsNull()) { + SL_WARN(logger_, "Validator registry YAML is empty"); + return; + } + + if (not root.IsMap()) { + qtils::raise(ValidatorRegistryError::MAP_EXPECTED); + } + + for (const auto &entry : root) { + if (not entry.first.IsScalar()) { + qtils::raise(ValidatorRegistryError::NODE_ID_SCALAR_EXPECTED); + } + auto node_id = entry.first.as(); + const auto &indices_node = entry.second; + + std::vector indices; + auto parse_index = [&](const YAML::Node &value) { + if (not value.IsScalar()) { + qtils::raise(ValidatorRegistryError::INDICES_SCALAR_EXPECTED); + } + auto idx = value.as(); + indices.emplace_back(idx); + }; + + if (indices_node.IsSequence()) { + for (const auto &child : indices_node) { + parse_index(child); + } + } else if (indices_node.IsScalar()) { + parse_index(indices_node); + } else if (indices_node.IsNull()) { + continue; + } else { + qtils::raise(ValidatorRegistryError::INVALID_VALUE_TYPE); + } + + if (indices.empty()) { + SL_WARN(logger_, + "Validator registry entry '{}' has no validator indices", + node_id); + continue; + } + + auto &node_indices = node_to_indices_[node_id]; + node_indices.reserve(node_indices.size() + indices.size()); + + for (auto idx : indices) { + if (index_to_node_.contains(idx)) { + qtils::raise(ValidatorRegistryError::ALREADY_ASSIGNED); + } + index_to_node_.emplace(idx, node_id); + node_indices.emplace_back(idx); + } + } + + if (current_node_id_.empty()) { + SL_WARN(logger_, "Node id is not set; defaulting validator index to 0"); + return; + } + + if (auto opt_indices = validatorIndicesForNodeId(current_node_id_); + opt_indices.has_value()) { + current_validator_indices_ = opt_indices.value(); + SL_INFO(logger_, + "Node '{}' mapped to validator indices {}", + current_node_id_, + fmt::join(current_validator_indices_, " ")); + } else { + SL_WARN(logger_, + "Validator indices for node '{}' not found in registry YAML", + current_node_id_); + } + } +} // namespace lean diff --git a/src/blockchain/impl/validator_registry_impl.hpp b/src/blockchain/impl/validator_registry_impl.hpp new file mode 100644 index 0000000..7c27f00 --- /dev/null +++ b/src/blockchain/impl/validator_registry_impl.hpp @@ -0,0 +1,72 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +#include "blockchain/validator_registry.hpp" + +namespace YAML { + class Node; +} // namespace YAML + +namespace lean::app { + class Configuration; +} // namespace lean::app + +namespace lean { + /** + * ValidatorRegistry provides access to mapping between validator indices and + * node identifiers. Mapping is loaded from YAML file with structure: + * + * ```yaml + * node_0: + * - 0 + * node_1: + * - 1 + * ``` + */ + class ValidatorRegistryImpl : public ValidatorRegistry { + public: + ValidatorRegistryImpl(qtils::SharedRef logging_system, + const app::Configuration &config); + ValidatorRegistryImpl(qtils::SharedRef logging_system, + std::string yaml, + std::string current_node_id); + + // ValidatorRegistry + [[nodiscard]] const ValidatorIndices ¤tValidatorIndices() + const override; + + [[nodiscard]] std::optional nodeIdByIndex( + ValidatorIndex index) const; + + [[nodiscard]] std::optional validatorIndicesForNodeId( + std::string_view node_id) const; + + private: + ValidatorRegistryImpl(qtils::SharedRef logging_system, + YAML::Node root, + std::string current_node_id); + + log::Logger logger_; + std::string current_node_id_; + std::unordered_map index_to_node_; + std::unordered_map> + node_to_indices_; + ValidatorIndices current_validator_indices_; + }; +} // namespace lean + +template <> +struct boost::di::ctor_traits { + BOOST_DI_INJECT_TRAITS(qtils::SharedRef, + const lean::app::Configuration &); +}; diff --git a/src/blockchain/state_transition_function.cpp b/src/blockchain/state_transition_function.cpp index 1571182..80856aa 100644 --- a/src/blockchain/state_transition_function.cpp +++ b/src/blockchain/state_transition_function.cpp @@ -38,9 +38,10 @@ namespace lean { auto &validators = state.justifications_validators.data(); Justifications justifications; size_t offset = 0; - BOOST_ASSERT(validators.size() == roots.size() * VALIDATOR_REGISTRY_LIMIT); + BOOST_ASSERT(validators.size() + == roots.size() * state.config.num_validators); for (auto &root : roots) { - auto next_offset = offset + VALIDATOR_REGISTRY_LIMIT; + auto next_offset = offset + state.config.num_validators; std::vector bits{ validators.begin() + offset, validators.begin() + next_offset, @@ -62,9 +63,9 @@ namespace lean { roots.clear(); roots.reserve(justifications.size()); validators.clear(); - validators.reserve(justifications.size() * VALIDATOR_REGISTRY_LIMIT); + validators.reserve(justifications.size() * state.config.num_validators); for (auto &[root, bits] : justifications) { - BOOST_ASSERT(bits.size() == VALIDATOR_REGISTRY_LIMIT); + BOOST_ASSERT(bits.size() == state.config.num_validators); roots.push_back(root); validators.insert(validators.end(), bits.begin(), bits.end()); } @@ -102,10 +103,9 @@ namespace lean { return result; } - outcome::result STF::stateTransition(const SignedBlock &signed_block, + outcome::result STF::stateTransition(const Block &block, const State &parent_state, bool check_state_root) const { - auto &block = signed_block.message; auto state = parent_state; // Process slots (including those with no blocks) since block OUTCOME_TRY(processSlots(state, block.slot)); @@ -245,13 +245,13 @@ namespace lean { if (justifications_it == justifications.end()) { justifications_it = justifications.emplace(vote.target.root, std::vector{}).first; - justifications_it->second.resize(VALIDATOR_REGISTRY_LIMIT); + justifications_it->second.resize(state.config.num_validators); } - if (vote.validator_id >= justifications_it->second.size()) { + if (signed_vote.validator_id >= justifications_it->second.size()) { return Error::INVALID_VOTER; } - justifications_it->second.at(vote.validator_id) = true; + justifications_it->second.at(signed_vote.validator_id) = true; size_t count = std::ranges::count(justifications_it->second, true); diff --git a/src/blockchain/state_transition_function.hpp b/src/blockchain/state_transition_function.hpp index 64b9597..8111467 100644 --- a/src/blockchain/state_transition_function.hpp +++ b/src/blockchain/state_transition_function.hpp @@ -18,7 +18,6 @@ namespace lean { struct Block; struct BlockBody; struct Config; - struct SignedBlock; struct SignedVote; struct State; @@ -61,7 +60,7 @@ namespace lean { * Apply block to parent state. * @returns new state */ - outcome::result stateTransition(const SignedBlock &signed_block, + outcome::result stateTransition(const Block &block, const State &parent_state, bool check_state_root) const; diff --git a/src/blockchain/validator_registry.hpp b/src/blockchain/validator_registry.hpp new file mode 100644 index 0000000..254ef5a --- /dev/null +++ b/src/blockchain/validator_registry.hpp @@ -0,0 +1,23 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "types/validator_index.hpp" + +namespace lean { + class ValidatorRegistry { + public: + using ValidatorIndices = std::unordered_set; + + virtual ~ValidatorRegistry() = default; + + [[nodiscard]] virtual const ValidatorIndices ¤tValidatorIndices() + const = 0; + }; +} // namespace lean diff --git a/src/executable/lean_node.cpp b/src/executable/lean_node.cpp index 03c34e8..b59ce3a 100644 --- a/src/executable/lean_node.cpp +++ b/src/executable/lean_node.cpp @@ -7,7 +7,9 @@ #include #include #include +#include +#include #include #include #include @@ -39,9 +41,8 @@ namespace { using lean::log::LoggingSystem; int run_node(std::shared_ptr logsys, - std::shared_ptr appcfg, - std::shared_ptr genesis_cfg) { - auto injector = std::make_unique(logsys, appcfg, genesis_cfg); + std::shared_ptr appcfg) { + auto injector = std::make_unique(logsys, appcfg); // Load modules std::deque> loaders; @@ -101,6 +102,9 @@ namespace { } // namespace int main(int argc, const char **argv, const char **env) { + setlinebuf(stdout); + setlinebuf(stderr); + soralog::util::setThreadName("lean-node"); auto getArg = [&](size_t i) { @@ -199,28 +203,14 @@ int main(int argc, const char **argv, const char **env) { config_res.value(); }); - // set genesis config. Genesis time should be next multiple of 12 seconds - // since epoch (in ms) - constexpr uint64_t GENESIS_INTERVAL_MS = 12'000; // 12 seconds in milliseconds - uint64_t genesis_time = - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - genesis_time += GENESIS_INTERVAL_MS - (genesis_time % GENESIS_INTERVAL_MS); - - lean::Config genesis_config{.num_validators = 4, - .genesis_time = genesis_time}; - int exit_code; - + auto logger = logging_system->getLogger("Main", lean::log::defaultGroupName); { std::string_view name{argv[1]}; if (name.substr(0, 1) == "-") { // The first argument isn't subcommand, run as node - exit_code = run_node(logging_system, - app_configuration, - std::make_shared(genesis_config)); + exit_code = run_node(logging_system, app_configuration); } // else if (false and name == "subcommand-1"s) { @@ -238,7 +228,6 @@ int main(int argc, const char **argv, const char **env) { } } - auto logger = logging_system->getLogger("Main", lean::log::defaultGroupName); SL_INFO(logger, "All components are stopped"); logger->flush(); diff --git a/src/injector/CMakeLists.txt b/src/injector/CMakeLists.txt index 3089c88..073b7e8 100644 --- a/src/injector/CMakeLists.txt +++ b/src/injector/CMakeLists.txt @@ -24,4 +24,5 @@ target_link_libraries(node_injector storage timeline p2p::p2p_key_validator + validator_registry ) diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 0aa2888..818bffd 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -27,10 +27,11 @@ #include "app/impl/state_manager_impl.hpp" #include "app/impl/timeline_impl.hpp" #include "app/impl/watchdog.hpp" +#include "blockchain/genesis_config.hpp" #include "blockchain/impl/block_storage_impl.hpp" #include "blockchain/impl/block_tree_impl.hpp" #include "blockchain/impl/fc_block_tree.hpp" -#include "blockchain/impl/genesis_block_header_impl.hpp" +#include "blockchain/impl/validator_registry_impl.hpp" #include "clock/impl/clock_impl.hpp" #include "crypto/hasher/hasher_impl.hpp" #include "injector/bind_by_lambda.hpp" @@ -68,7 +69,6 @@ namespace { template auto makeApplicationInjector(std::shared_ptr logsys, std::shared_ptr app_config, - std::shared_ptr genesis_config, Ts &&...args) { // clang-format off return di::make_injector( @@ -89,16 +89,15 @@ namespace { .openmetricsHttpEndpoint() }; }), - di::bind.to(genesis_config), di::bind.to(), //di::bind.to(), di::bind.to(), di::bind.to(), di::bind.to(), - di::bind.to(), di::bind.to(), di::bind.to(), di::bind.to(), + di::bind.to(), // user-defined overrides... std::forward(args)...); @@ -108,17 +107,9 @@ namespace { template auto makeNodeInjector(std::shared_ptr logsys, std::shared_ptr app_config, - std::shared_ptr genesis_config, Ts &&...args) { - AnchorState genesis_state = STF::generateGenesisState(*genesis_config); - AnchorBlock genesis_block = STF::genesisBlock(genesis_state); - return di::make_injector( - makeApplicationInjector(std::move(logsys), - std::move(app_config), - std::move(genesis_config), - useConfig(genesis_state), - useConfig(genesis_block)), + makeApplicationInjector(std::move(logsys), std::move(app_config)), // user-defined overrides... std::forward(args)...); @@ -130,8 +121,7 @@ namespace lean::injector { public: using Injector = decltype(makeNodeInjector(std::shared_ptr(), - std::shared_ptr(), - std::shared_ptr())); + std::shared_ptr())); explicit NodeInjectorImpl(Injector injector) : injector_{std::move(injector)} {} @@ -140,12 +130,9 @@ namespace lean::injector { }; NodeInjector::NodeInjector(std::shared_ptr logsys, - std::shared_ptr app_config, - std::shared_ptr genesis_config) + std::shared_ptr app_config) : pimpl_{std::make_unique( - makeNodeInjector(std::move(logsys), - std::move(app_config), - std::move(genesis_config)))} {} + makeNodeInjector(std::move(logsys), std::move(app_config)))} {} std::shared_ptr NodeInjector::injectApplication() { return pimpl_->injector_ diff --git a/src/injector/node_injector.hpp b/src/injector/node_injector.hpp index 015c920..a564c0b 100644 --- a/src/injector/node_injector.hpp +++ b/src/injector/node_injector.hpp @@ -13,9 +13,6 @@ namespace lean { class ForkChoiceStore; } -namespace lean { - struct Config; -} namespace lean::log { class LoggingSystem; } // namespace lean::log @@ -42,8 +39,7 @@ namespace lean::injector { class NodeInjector final { public: explicit NodeInjector(std::shared_ptr logging_system, - std::shared_ptr app_config, - std::shared_ptr genesis_config); + std::shared_ptr app_config); std::shared_ptr injectApplication(); std::unique_ptr register_loader( diff --git a/src/loaders/impl/networking_loader.hpp b/src/loaders/impl/networking_loader.hpp index 5586a55..3f57e90 100644 --- a/src/loaders/impl/networking_loader.hpp +++ b/src/loaders/impl/networking_loader.hpp @@ -17,10 +17,19 @@ #include "modules/shared/networking_types.tmp.hpp" #include "se/subscription.hpp" +namespace lean { + class ForkChoiceStore; +} // namespace lean + namespace lean::blockchain { class BlockTree; } // namespace lean::blockchain +namespace lean::app { + class ChainSpec; + class Configuration; +} // namespace lean::app + namespace lean::loaders { class NetworkingLoader final @@ -30,6 +39,8 @@ namespace lean::loaders { log::Logger logger_; qtils::SharedRef block_tree_; qtils::SharedRef fork_choice_store_; + qtils::SharedRef chain_spec_; + qtils::SharedRef app_config_; std::shared_ptr> on_init_complete_; @@ -48,11 +59,15 @@ namespace lean::loaders { NetworkingLoader(std::shared_ptr logsys, std::shared_ptr se_manager, qtils::SharedRef block_tree, - qtils::SharedRef fork_choice_store) + qtils::SharedRef fork_choice_store, + qtils::SharedRef chain_spec, + qtils::SharedRef app_config) : Loader(std::move(logsys), std::move(se_manager)), logger_(logsys_->getLogger("Networking", "networking_module")), block_tree_{std::move(block_tree)}, - fork_choice_store_{std::move(fork_choice_store)} {} + fork_choice_store_{std::move(fork_choice_store)}, + chain_spec_{std::move(chain_spec)}, + app_config_{std::move(app_config)} {} NetworkingLoader(const NetworkingLoader &) = delete; NetworkingLoader &operator=(const NetworkingLoader &) = delete; @@ -67,15 +82,21 @@ namespace lean::loaders { modules::NetworkingLoader &, std::shared_ptr, qtils::SharedRef, - qtils::SharedRef>( + qtils::SharedRef, + qtils::SharedRef, + qtils::SharedRef>( "query_module_instance"); if (not module_accessor) { return; } - auto module_internal = - (*module_accessor)(*this, logsys_, block_tree_, fork_choice_store_); + auto module_internal = (*module_accessor)(*this, + logsys_, + block_tree_, + fork_choice_store_, + chain_spec_, + app_config_); on_init_complete_ = se::SubscriberCreator::template create< EventTypes::NetworkingIsLoaded>( diff --git a/src/log/formatters/block_index_ref.hpp b/src/log/formatters/block_index_ref.hpp new file mode 100644 index 0000000..a867859 --- /dev/null +++ b/src/log/formatters/block_index_ref.hpp @@ -0,0 +1,49 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "types/block_hash.hpp" +#include "types/slot.hpp" + +namespace lean { + struct BlockIndexRef { + Slot slot; + const BlockHash &hash; + }; +} // namespace lean + +template <> +struct fmt::formatter { + // Presentation format + bool long_form = false; + + // Parses format specifications of the form ['s' | 'l']. + constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { + auto it = ctx.begin(), end = ctx.end(); + if (it != end) { + if (*it == 'l' or *it == 's') { + long_form = *it == 'l'; + ++it; + } + } + if (it != end && *it != '}') { + throw format_error("invalid format"); + } + return it; + } + + // Formats the BlockIndex using the parsed format specification (presentation) + // stored in this formatter. + template + auto format(const lean::BlockIndexRef &v, FormatContext &ctx) const + -> decltype(ctx.out()) { + [[unlikely]] if (long_form) { + return fmt::format_to(ctx.out(), "{:0xx} @ {}", v.hash, v.slot); + } + return fmt::format_to(ctx.out(), "{:0x} @ {}", v.hash, v.slot); + } +}; diff --git a/src/log/formatters/filepath.hpp b/src/log/formatters/filepath.hpp index 12bc599..c4f14ed 100644 --- a/src/log/formatters/filepath.hpp +++ b/src/log/formatters/filepath.hpp @@ -13,7 +13,7 @@ template <> struct fmt::formatter : fmt::formatter { - auto format(const std::filesystem::path &path, format_context &ctx) { + auto format(const std::filesystem::path &path, format_context &ctx) const { return fmt::formatter::format(path.native(), ctx); } }; diff --git a/src/modules/networking/CMakeLists.txt b/src/modules/networking/CMakeLists.txt index 4f2bc9c..3389730 100644 --- a/src/modules/networking/CMakeLists.txt +++ b/src/modules/networking/CMakeLists.txt @@ -16,10 +16,12 @@ add_lean_module(networking DEFINITIONS SOME_FLAG=1 LIBRARIES + blockchain + bootnodes p2p::libp2p qtils::qtils Snappy::snappy soralog::soralog - blockchain sszpp + validator_registry ) diff --git a/src/modules/networking/get_node_key.hpp b/src/modules/networking/get_node_key.hpp new file mode 100644 index 0000000..de896e6 --- /dev/null +++ b/src/modules/networking/get_node_key.hpp @@ -0,0 +1,65 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace lean { + namespace detail { + inline libp2p::crypto::secp256k1::Secp256k1ProviderImpl & + secp256k1Provider() { + static libp2p::crypto::secp256k1::Secp256k1ProviderImpl provider{ + std::make_shared()}; + return provider; + } + + inline libp2p::crypto::KeyPair toKeyPair( + const libp2p::crypto::secp256k1::KeyPair &keypair) { + return libp2p::crypto::KeyPair{ + .publicKey = {{ + .type = libp2p::crypto::Key::Type::Secp256k1, + .data = qtils::ByteVec{keypair.public_key}, + }}, + .privateKey = {{ + .type = libp2p::crypto::Key::Type::Secp256k1, + .data = qtils::ByteVec{keypair.private_key}, + }}, + }; + } + } // namespace detail + + inline outcome::result keyPairFromPrivateKeyHex( + std::string_view hex_or_path) { + std::string hex{hex_or_path}; + boost::trim(hex); + libp2p::crypto::secp256k1::KeyPair keypair; + auto unhex_result = qtils::unhex0x(keypair.private_key, hex, true); + if (not unhex_result.has_value() and std::filesystem::exists(hex_or_path)) { + BOOST_OUTCOME_TRY(hex, qtils::readText(hex_or_path)); + boost::trim(hex); + unhex_result = qtils::unhex0x(keypair.private_key, hex, true); + } + BOOST_OUTCOME_TRY(unhex_result); + BOOST_OUTCOME_TRY(keypair.public_key, + detail::secp256k1Provider().derive(keypair.private_key)); + return detail::toKeyPair(keypair); + } + + inline libp2p::crypto::KeyPair randomKeyPair() { + auto keypair = detail::secp256k1Provider().generate().value(); + return detail::toKeyPair(keypair); + } +} // namespace lean diff --git a/src/modules/networking/module.cpp b/src/modules/networking/module.cpp index 6381900..6b35211 100644 --- a/src/modules/networking/module.cpp +++ b/src/modules/networking/module.cpp @@ -13,6 +13,11 @@ namespace lean::blockchain { class BlockTree; } // namespace lean::blockchain +namespace lean::app { + class ChainSpec; + class Configuration; +} // namespace lean::app + MODULE_C_API const char *loader_id() { return "NetworkingLoader"; } @@ -30,13 +35,22 @@ MODULE_C_API std::weak_ptr query_module_instance( lean::modules::NetworkingLoader &loader, std::shared_ptr logsys, qtils::SharedRef block_tree, - qtils::SharedRef fork_choice_store) { + qtils::SharedRef fork_choice_store, + qtils::SharedRef chain_spec, + qtils::SharedRef app_config) { if (!module_instance) { BOOST_ASSERT(logsys); BOOST_ASSERT(block_tree); BOOST_ASSERT(fork_choice_store); - module_instance = lean::modules::NetworkingImpl::create_shared( - loader, std::move(logsys), block_tree, fork_choice_store); + BOOST_ASSERT(chain_spec); + BOOST_ASSERT(app_config); + module_instance = + lean::modules::NetworkingImpl::create_shared(loader, + std::move(logsys), + block_tree, + fork_choice_store, + chain_spec, + app_config); } return module_instance; } diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index 19bf112..303e7e8 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -7,16 +7,26 @@ #include "modules/networking/networking.hpp" +#include +#include +#include + +#include #include #include #include #include #include +#include #include #include #include +#include #include +#include +#include #include +#include #include #include "blockchain/block_tree.hpp" @@ -25,8 +35,6 @@ #include "modules/networking/ssz_snappy.hpp" #include "modules/networking/status_protocol.hpp" #include "modules/networking/types.hpp" -#include "utils/__debug_env.hpp" -#include "utils/sample_peer.hpp" namespace lean::modules { // TODO(turuslan): gossip [from,seqno,signature,key]=None @@ -65,11 +73,15 @@ namespace lean::modules { NetworkingLoader &loader, qtils::SharedRef logging_system, qtils::SharedRef block_tree, - qtils::SharedRef fork_choice_store) + qtils::SharedRef fork_choice_store, + qtils::SharedRef chain_spec, + qtils::SharedRef config) : loader_(loader), logger_(logging_system->getLogger("Networking", "networking_module")), block_tree_{std::move(block_tree)}, - fork_choice_store_{std::move(fork_choice_store)} { + fork_choice_store_{std::move(fork_choice_store)}, + chain_spec_{std::move(chain_spec)}, + config_{std::move(config)} { libp2p::log::setLoggingSystem(logging_system->getSoralog()); block_tree_ = std::make_shared(fork_choice_store_); } @@ -84,10 +96,28 @@ namespace lean::modules { void NetworkingImpl::on_loaded_success() { SL_INFO(logger_, "Loaded success"); - SamplePeer sample_peer{getPeerIndex()}; + auto keypair = config_->nodeKey(); + + // Always set up identity and peer info + libp2p::peer::IdentityManager identity_manager{ + keypair, + std::make_shared(nullptr)}; + auto peer_id = identity_manager.getId(); + + SL_INFO(logger_, "Networking loaded with PeerId {}", peer_id.toBase58()); + + libp2p::protocol::gossip::Config gossip_config; + gossip_config.validation_mode = + libp2p::protocol::gossip::ValidationMode::Anonymous; + gossip_config.message_authenticity = + libp2p::protocol::gossip::MessageAuthenticity::Anonymous; + gossip_config.message_id_fn = gossipMessageId; + gossip_config.protocol_versions.erase( + libp2p::protocol::gossip::kProtocolGossipsubv1_2); auto injector = qtils::toSharedPtr(libp2p::injector::makeHostInjector( - libp2p::injector::useKeyPair(sample_peer.keypair), + libp2p::injector::useKeyPair(keypair), + libp2p::injector::useGossipConfig(std::move(gossip_config)), libp2p::injector::useTransportAdaptors< libp2p::transport::QuicTransport>())); injector_ = injector; @@ -95,11 +125,102 @@ namespace lean::modules { auto host = injector->create>(); - if (auto r = host->listen(sample_peer.listen); not r.has_value()) { - SL_WARN(logger_, "listen {} error: {}", sample_peer.listen, r.error()); + bool has_enr_listen_address = false; + const auto &bootnodes = chain_spec_->getBootnodes(); + for (auto &bootnode : bootnodes.getBootnodes()) { + if (bootnode.peer_id != peer_id) { + continue; + } + has_enr_listen_address = true; + auto info_res = libp2p::transport::detail::asQuic(bootnode.address); + if (not info_res.has_value()) { + SL_WARN(logger_, "Incompatible enr address {}", bootnode.address); + continue; + } + auto port = info_res.value().port; + auto enr_listen_address = + libp2p::multi::Multiaddress::create( + std::format("/ip4/0.0.0.0/udp/{}/quic-v1", port)) + .value(); + if (auto r = host->listen(enr_listen_address); not r.has_value()) { + SL_WARN(logger_, + "Error listening on enr address {}: {}", + enr_listen_address, + r.error()); + } else { + SL_INFO(logger_, "Listening on {}", enr_listen_address); + } + } + + if (auto &listen_addr = config_->listenMultiaddr(); + listen_addr.has_value()) { + auto &listen = *listen_addr; + if (auto r = host->listen(listen); not r.has_value()) { + SL_WARN(logger_, + "Listening address configured via --listen-addr: {}", + listen); + } else { + SL_INFO(logger_, "Listening on {}", listen); + } + } else if (not has_enr_listen_address) { + SL_WARN(logger_, "No listen multiaddress configured"); } host->start(); + // Add bootnodes from chain spec + if (!bootnodes.empty()) { + SL_INFO(logger_, + "Adding {} bootnodes to address repository", + bootnodes.size()); + + auto &address_repo = host->getPeerRepository().getAddressRepository(); + + for (const auto &bootnode : bootnodes.getBootnodes()) { + if (bootnode.peer_id == peer_id) { + continue; + } + std::vector addresses{bootnode.address}; + + // Add bootnode addresses with permanent TTL + auto result = address_repo.upsertAddresses( + bootnode.peer_id, + std::span(addresses), + libp2p::peer::ttl::kPermanent); + + if (result.has_value()) { + SL_DEBUG(logger_, + "Added bootnode: peer={}, address={}", + bootnode.peer_id, + bootnode.address.getStringAddress()); + } else { + SL_WARN(logger_, + "Failed to add bootnode: peer={}, error={}", + bootnode.peer_id, + result.error()); + } + libp2p::PeerInfo peer_info{.id = bootnode.peer_id, + .addresses = addresses}; + libp2p::coroSpawn(*io_context_, + [weak_self{weak_from_this()}, + host, + peer_info]() -> libp2p::Coro { + if (auto r = co_await host->connect(peer_info); + not r.has_value()) { + auto self = weak_self.lock(); + if (not self) { + co_return; + } + SL_WARN(self->logger_, + "connect {} error: {}", + peer_info.id, + r.error()); + } + }); + } + } else { + SL_DEBUG(logger_, "No bootnodes configured"); + } + auto on_peer_connected = [weak_self{weak_from_this()}]( std::weak_ptr @@ -168,7 +289,12 @@ namespace lean::modules { gossip_ = injector->create>(); + ping_ = injector->create>(); + identify_ = injector->create>(); + gossip_->start(); + ping_->start(); + identify_->start(); gossip_blocks_topic_ = gossipSubscribe( "block", [weak_self{weak_from_this()}](SignedBlock &&block) { @@ -187,33 +313,18 @@ namespace lean::modules { } auto res = self->fork_choice_store_->processAttestation(signed_vote, false); - BOOST_ASSERT_MSG(res.has_value(), "Gossiped vote should be valid"); + if (not res.has_value()) { + SL_WARN(self->logger_, + "Error processing vote for target {}: {}", + signed_vote.data.target, + res.error()); + return; + } SL_INFO(self->logger_, - "Received vote for target {}@{}", - signed_vote.data.target.slot, - signed_vote.data.target.root); + "Received vote for target {}", + signed_vote.data.target); }); - if (sample_peer.index != 0) { - libp2p::coroSpawn( - *io_context_, - [weak_self{weak_from_this()}, host]() -> libp2p::Coro { - SamplePeer sample_peer{0}; - - if (auto r = co_await host->connect(sample_peer.connect_info); - not r.has_value()) { - auto self = weak_self.lock(); - if (not self) { - co_return; - } - SL_WARN(self->logger_, - "connect {} error: {}", - sample_peer.connect, - r.error()); - } - }); - } - io_thread_.emplace([io_context{io_context_}] { auto work_guard = boost::asio::make_work_guard(*io_context); io_context->run(); @@ -308,9 +419,10 @@ namespace lean::modules { SignedBlock &&signed_block) { auto slot_hash = signed_block.message.slotHash(); SL_DEBUG(logger_, - "receiveBlock slot {} hash {}", + "receiveBlock slot {} hash {} parent {}", slot_hash.slot, - slot_hash.hash); + slot_hash.hash, + signed_block.message.parent_root); auto remove = [&](auto f) { std::vector queue{slot_hash.hash}; while (not queue.empty()) { @@ -344,14 +456,24 @@ namespace lean::modules { remove([&](const BlockHash &block_hash) { blocks.emplace_back(block_cache_.extract(block_hash).mapped()); }); - std::string __s; for (auto &block : blocks) { - __s += std::format(" {}", block.message.slot); auto res = fork_choice_store_->onBlock(block.message); - BOOST_ASSERT_MSG(res.has_value(), - "Fork choice store should accept imported block"); + if (not res.has_value()) { + SL_WARN(logger_, + "Error importing block {}: {}", + block.message.slotHash(), + res.error()); + break; + } } - SL_INFO(logger_, "receiveBlock {} => import{}", slot_hash.slot, __s); + SL_INFO(logger_, + "receiveBlock {} => import {}", + slot_hash.slot, + fmt::join( + blocks | std::views::transform([](const SignedBlock &block) { + return block.message.slot; + }), + " ")); block_tree_->import(std::move(blocks)); return; } diff --git a/src/modules/networking/networking.hpp b/src/modules/networking/networking.hpp index 7d11425..6dc42a8 100644 --- a/src/modules/networking/networking.hpp +++ b/src/modules/networking/networking.hpp @@ -8,7 +8,6 @@ #include -#include #include #include #include @@ -20,15 +19,29 @@ namespace boost::asio { class io_context; } // namespace boost::asio -namespace lean::blockchain { - class BlockTree; -} // namespace lean::blockchain +namespace libp2p::protocol { + class Ping; + class Identify; +} // namespace libp2p::protocol namespace libp2p::protocol::gossip { class Gossip; class Topic; } // namespace libp2p::protocol::gossip +namespace lean { + class ForkChoiceStore; +} // namespace lean + +namespace lean::blockchain { + class BlockTree; +} // namespace lean::blockchain + +namespace lean::app { + class ChainSpec; + class Configuration; +} // namespace lean::app + namespace lean::modules { class StatusProtocol; class BlockRequestProtocol; @@ -49,7 +62,9 @@ namespace lean::modules { NetworkingImpl(NetworkingLoader &loader, qtils::SharedRef logging_system, qtils::SharedRef block_tree, - qtils::SharedRef fork_choice_store); + qtils::SharedRef fork_choice_store, + qtils::SharedRef chain_spec, + qtils::SharedRef config); public: CREATE_SHARED_METHOD(NetworkingImpl); @@ -80,6 +95,8 @@ namespace lean::modules { log::Logger logger_; qtils::SharedRef block_tree_; qtils::SharedRef fork_choice_store_; + qtils::SharedRef chain_spec_; + qtils::SharedRef config_; std::shared_ptr injector_; std::shared_ptr io_context_; std::optional io_thread_; @@ -88,6 +105,8 @@ namespace lean::modules { std::shared_ptr status_protocol_; std::shared_ptr block_request_protocol_; std::shared_ptr gossip_; + std::shared_ptr ping_; + std::shared_ptr identify_; std::shared_ptr gossip_blocks_topic_; std::shared_ptr gossip_votes_topic_; std::unordered_map block_cache_; diff --git a/src/modules/networking/read_nodes_yaml.hpp b/src/modules/networking/read_nodes_yaml.hpp new file mode 100644 index 0000000..2d498d9 --- /dev/null +++ b/src/modules/networking/read_nodes_yaml.hpp @@ -0,0 +1,70 @@ +/** +* Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "serde/enr.hpp" + +namespace lean { + struct ParsedEnr { + std::string raw; + enr::Enr enr; + }; + + struct NodesYamlParseResult { + std::vector parsed; + std::vector invalid_entries; + }; + + enum class NodesYamlError { + INVALID, + }; + Q_ENUM_ERROR_CODE(NodesYamlError) { + using E = decltype(e); + switch (e) { + case E::INVALID: + return "Invalid nodes.yaml"; + } + abort(); + } + + /** + * Read ENR from nodes.yaml + */ + inline outcome::result readNodesYaml( + const std::filesystem::path &path) { + NodesYamlParseResult result; + auto yaml = YAML::LoadFile(path); + if (not yaml.IsSequence()) { + return NodesYamlError::INVALID; + } + for (auto &&yaml_enr : yaml) { + if (not yaml_enr.IsScalar()) { + return NodesYamlError::INVALID; + } + auto enr_str = yaml_enr.as(); + if (enr_str.empty()) { + result.invalid_entries.emplace_back(std::move(enr_str)); + continue; + } + auto enr_res = enr::decode(enr_str); + if (not enr_res.has_value()) { + result.invalid_entries.emplace_back(std::move(enr_str)); + continue; + } + result.parsed.emplace_back(ParsedEnr{std::move(enr_str), + std::move(enr_res.value())}); + } + return result; + } +} // namespace lean \ No newline at end of file diff --git a/src/modules/production/production.cpp b/src/modules/production/production.cpp index eb546d8..e0a289c 100644 --- a/src/modules/production/production.cpp +++ b/src/modules/production/production.cpp @@ -14,7 +14,6 @@ #include "modules/shared/prodution_types.tmp.hpp" #include "types/block_data.hpp" #include "types/signed_block.hpp" -#include "utils/__debug_env.hpp" namespace lean::modules { ProductionModuleImpl::ProductionModuleImpl( diff --git a/src/modules/production/read_config_yaml.hpp b/src/modules/production/read_config_yaml.hpp new file mode 100644 index 0000000..23ea5b1 --- /dev/null +++ b/src/modules/production/read_config_yaml.hpp @@ -0,0 +1,52 @@ +/** +* Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include + +#include "types/config.hpp" + +namespace lean { + enum class ConfigYamlError { + INVALID, + }; + Q_ENUM_ERROR_CODE(ConfigYamlError) { + using E = decltype(e); + switch (e) { + case E::INVALID: + return "Invalid config.yaml"; + } + abort(); + } + + /** + * Read GENESIS_TIME and VALIDATOR_COUNT from config.yaml + */ + inline outcome::result readConfigYaml( + const std::filesystem::path &path) { + auto yaml = YAML::LoadFile(path); + if (not yaml.IsMap()) { + return ConfigYamlError::INVALID; + } + auto yaml_genesis_time = yaml["GENESIS_TIME"]; + if (not yaml_genesis_time.IsScalar()) { + return ConfigYamlError::INVALID; + } + auto yaml_validator_count = yaml["VALIDATOR_COUNT"]; + if (not yaml_validator_count.IsScalar()) { + return ConfigYamlError::INVALID; + } + return Config{ + .num_validators = yaml_validator_count.as(), + .genesis_time = yaml_genesis_time.as(), + }; + } +} // namespace lean \ No newline at end of file diff --git a/src/qtils/value_or_raise.hpp b/src/qtils/value_or_raise.hpp new file mode 100644 index 0000000..b79e4c6 --- /dev/null +++ b/src/qtils/value_or_raise.hpp @@ -0,0 +1,23 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + /** + * Get value or `outcome::raise`. + * Same as `.value()`. + */ + template + auto valueOrRaise(outcome::result r) { + if (not r.has_value()) { + raise(r.error()); + } + return std::move(r).value(); + } +} // namespace qtils diff --git a/src/serde/enr.cpp b/src/serde/enr.cpp index 476ca6f..d1cc935 100644 --- a/src/serde/enr.cpp +++ b/src/serde/enr.cpp @@ -280,10 +280,10 @@ namespace lean::enr { auto &ip = this->ip.value(); return libp2p::Multiaddress::create( std::format("/ip4/{}.{}.{}.{}/udp/{}/quic-v1/p2p/{}", - ip[3], - ip[2], - ip[1], ip[0], + ip[1], + ip[2], + ip[3], port.value(), peerId().toBase58())) .value(); @@ -328,16 +328,16 @@ namespace lean::enr { BOOST_OUTCOME_TRY(enr.public_key, kv_secp256k1->second.bytes_n()); - auto kv_udp = kv.find("udp"); - if (kv_udp != kv.end()) { - BOOST_OUTCOME_TRY(enr.port, kv_udp->second.uint()); + auto kv_quic = kv.find("quic"); + if (kv_quic != kv.end()) { + BOOST_OUTCOME_TRY(enr.port, kv_quic->second.uint()); } return enr; } std::string encode(const Secp256k1PublicKey &public_key, Port port) { - Enr enr{Secp256k1Signature{}, 1, public_key, Ip{1, 0, 0, 127}, port}; + Enr enr{Secp256k1Signature{}, 1, public_key, Ip{127, 0, 0, 1}, port}; rlp::Encoder rlp; rlp.bytes(enr.signature); rlp.uint(enr.sequence); @@ -345,10 +345,10 @@ namespace lean::enr { rlp.str("v4"); rlp.str("ip"); rlp.bytes(enr.ip.value()); + rlp.str("quic"); + rlp.uint(enr.port.value()); rlp.str("secp256k1"); rlp.bytes(enr.public_key); - rlp.str("udp"); - rlp.uint(enr.port.value()); return "enr:" + cppcodec::base64_url_unpadded::encode(rlp.list()); } } // namespace lean::enr diff --git a/src/types/block_index.hpp b/src/types/block_index.hpp index f3e1958..e85cdbc 100644 --- a/src/types/block_index.hpp +++ b/src/types/block_index.hpp @@ -6,7 +6,7 @@ #pragma once -#include "types/types.hpp" +#include "log/formatters/block_index_ref.hpp" namespace lean { @@ -31,35 +31,11 @@ struct std::hash { }; template <> -struct fmt::formatter { - // Presentation format - bool long_form = false; - - // Parses format specifications of the form ['s' | 'l']. - constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { - auto it = ctx.begin(), end = ctx.end(); - if (it != end) { - if (*it == 'l' or *it == 's') { - long_form = *it == 'l'; - ++it; - } - } - if (it != end && *it != '}') { - throw format_error("invalid format"); - } - return it; - } - - // Formats the BlockIndex using the parsed format specification (presentation) - // stored in this formatter. +struct fmt::formatter : fmt::formatter { template auto format(const lean::BlockIndex &block_index, FormatContext &ctx) const -> decltype(ctx.out()) { - [[unlikely]] if (long_form) { - return fmt::format_to( - ctx.out(), "{:0xx} @ {}", block_index.hash, block_index.slot); - } - return fmt::format_to( - ctx.out(), "{:0x} @ {}", block_index.hash, block_index.slot); + return fmt::formatter::format( + lean::BlockIndexRef{block_index.slot, block_index.hash}, ctx); } }; diff --git a/src/types/block_signature.hpp b/src/types/block_signature.hpp index 8b4d8b8..a680166 100644 --- a/src/types/block_signature.hpp +++ b/src/types/block_signature.hpp @@ -6,10 +6,9 @@ #pragma once -#include "types.hpp" +#include namespace lean { - - using BlockSignature = OpaqueHash; - + // https://github.com/ReamLabs/ream/blob/5a4b3cb42d5646a0d12ec1825ace03645dbfd59b/crates/common/consensus/lean/src/block.rs#L15-L18 + using BlockSignature = qtils::ByteArr<4000>; } diff --git a/src/types/checkpoint.hpp b/src/types/checkpoint.hpp index 90ffaf2..6bc06c5 100644 --- a/src/types/checkpoint.hpp +++ b/src/types/checkpoint.hpp @@ -6,15 +6,14 @@ #pragma once -#include #include -#include "types.hpp" +#include "log/formatters/block_index_ref.hpp" namespace lean { struct Checkpoint : ssz::ssz_container { - qtils::ByteArr<32> root; + BlockHash root; Slot slot = 0; static Checkpoint from(const auto &v) { @@ -25,3 +24,13 @@ namespace lean { }; } // namespace lean + +template <> +struct fmt::formatter : fmt::formatter { + template + auto format(const lean::Checkpoint &v, FormatContext &ctx) const + -> decltype(ctx.out()) { + return fmt::formatter::format( + lean::BlockIndexRef{v.slot, v.root}, ctx); + } +}; diff --git a/src/types/signed_block.hpp b/src/types/signed_block.hpp index 4996307..698b738 100644 --- a/src/types/signed_block.hpp +++ b/src/types/signed_block.hpp @@ -6,17 +6,22 @@ #pragma once -#include - #include "types/block.hpp" +#include "types/block_signature.hpp" namespace lean { - struct SignedBlock : ssz::ssz_container { + struct SignedBlock : ssz::ssz_variable_size_container { Block message; - qtils::ByteArr<32> signature; + BlockSignature signature; SSZ_CONT(message, signature); }; + /** + * Stub method to sign block. + */ + inline SignedBlock signBlock(Block block) { + return SignedBlock{.message = std::move(block)}; + } } // namespace lean diff --git a/src/types/signed_vote.hpp b/src/types/signed_vote.hpp index 2443c95..f50d573 100644 --- a/src/types/signed_vote.hpp +++ b/src/types/signed_vote.hpp @@ -6,18 +6,24 @@ #pragma once +#include "types/validator_index.hpp" #include "types/vote.hpp" +#include "types/vote_signature.hpp" namespace lean { struct SignedVote : ssz::ssz_container { + ValidatorIndex validator_id = 0; Vote data; - /// @note The signature type is still to be determined so Bytes32 is used in - /// the interim. The actual signature size is expected to be a lot larger - /// (~3 KiB). - qtils::ByteArr<32> signature; + VoteSignature signature; - SSZ_CONT(data, signature); + SSZ_CONT(validator_id, data, signature); }; + /** + * Stub method to sign vote. + */ + inline SignedVote signVote(ValidatorIndex validator_id, Vote vote) { + return SignedVote{.validator_id = validator_id, .data = std::move(vote)}; + } } // namespace lean diff --git a/src/types/vote.hpp b/src/types/vote.hpp index e70f4ba..1600ebc 100644 --- a/src/types/vote.hpp +++ b/src/types/vote.hpp @@ -11,13 +11,12 @@ namespace lean { struct Vote : public ssz::ssz_container { - uint64_t validator_id = 0; uint64_t slot = 0; Checkpoint head; Checkpoint target; Checkpoint source; - SSZ_CONT(validator_id, slot, head, target, source); + SSZ_CONT(slot, head, target, source); }; } // namespace lean diff --git a/src/types/vote_signature.hpp b/src/types/vote_signature.hpp new file mode 100644 index 0000000..9a78994 --- /dev/null +++ b/src/types/vote_signature.hpp @@ -0,0 +1,14 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace lean { + // https://github.com/ReamLabs/ream/blob/5a4b3cb42d5646a0d12ec1825ace03645dbfd59b/crates/common/consensus/lean/src/vote.rs#L13-L18 + using VoteSignature = qtils::ByteArr<4000>; +} diff --git a/src/utils/__debug_env.hpp b/src/utils/__debug_env.hpp deleted file mode 100644 index 6f23f6b..0000000 --- a/src/utils/__debug_env.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include - -// TODO(turuslan): config -inline size_t getPeerIndex() { - static size_t i = [] { - if (auto s = getenv("PeerIndex")) { - return std::stoul(s); - } else { - return 0ul; - } - }(); - return i; -} -inline size_t getValidatorCount() { - static size_t i = [] { - if (auto s = getenv("ValidatorCount")) { - return std::stoul(s); - } else { - return 1ul; - } - }(); - return i; -} diff --git a/tests/mock/app/chain_spec_mock.hpp b/tests/mock/app/chain_spec_mock.hpp index 5f88cfb..8d98a90 100644 --- a/tests/mock/app/chain_spec_mock.hpp +++ b/tests/mock/app/chain_spec_mock.hpp @@ -8,23 +8,17 @@ #include +#include "app/bootnodes.hpp" #include "app/chain_spec.hpp" namespace lean::app { class ChainSpecMock final : public ChainSpec { public: - MOCK_METHOD(const std::string &, id, (), (const, override)); - - MOCK_METHOD(const std::vector &, - bootNodes, + MOCK_METHOD(const app::Bootnodes &, + getBootnodes, (), (const, override)); - - MOCK_METHOD(const qtils::ByteVec &, genesisHeader, (), (const, override)); - - using KVMap = std::map; - MOCK_METHOD(const KVMap &, genesisState, (), (const, override)); }; } // namespace lean::app diff --git a/tests/mock/app/configuration_mock.hpp b/tests/mock/app/configuration_mock.hpp index 042ee1e..59ac8aa 100644 --- a/tests/mock/app/configuration_mock.hpp +++ b/tests/mock/app/configuration_mock.hpp @@ -18,9 +18,11 @@ namespace lean::app { // clang-format off MOCK_METHOD(const std::string&, nodeVersion, (), (const, override)); MOCK_METHOD(const std::string&, nodeName, (), (const, override)); + MOCK_METHOD(const std::string&, nodeId, (), (const, override)); MOCK_METHOD(const std::filesystem::path&, basePath, (), (const, override)); - MOCK_METHOD(const std::filesystem::path&, specFile, (), (const, override)); MOCK_METHOD(const std::filesystem::path&, modulesDir, (), (const, override)); + MOCK_METHOD(const std::filesystem::path&, bootnodesFile, (), (const, override)); + MOCK_METHOD(const std::filesystem::path&, validatorRegistryPath, (), (const, override)); MOCK_METHOD(const DatabaseConfig &, database, (), (const, override)); diff --git a/tests/mock/blockchain/validator_registry_mock.hpp b/tests/mock/blockchain/validator_registry_mock.hpp new file mode 100644 index 0000000..e1e4b56 --- /dev/null +++ b/tests/mock/blockchain/validator_registry_mock.hpp @@ -0,0 +1,21 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "blockchain/validator_registry.hpp" + +namespace lean { + class ValidatorRegistryMock : public ValidatorRegistry { + public: + MOCK_METHOD(const ValidatorIndices &, + currentValidatorIndices, + (), + (const, override)); + }; +} // namespace lean diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 5182bb1..b328ada 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -8,3 +8,4 @@ add_subdirectory(app) add_subdirectory(blockchain) add_subdirectory(storage) add_subdirectory(serde) +add_subdirectory(utils) diff --git a/tests/unit/app/CMakeLists.txt b/tests/unit/app/CMakeLists.txt index d42b5c2..e9c5ee3 100644 --- a/tests/unit/app/CMakeLists.txt +++ b/tests/unit/app/CMakeLists.txt @@ -12,6 +12,17 @@ # logger_for_tests # ) +addtest(bootnodes_test + bootnodes_test.cpp + ) +target_link_libraries(bootnodes_test + bootnodes + chain_spec + enr + logger_for_tests + p2p::libp2p + ) + addtest(app_state_manager_test state_manager_test.cpp ) diff --git a/tests/unit/app/bootnodes_test.cpp b/tests/unit/app/bootnodes_test.cpp new file mode 100644 index 0000000..8db3a41 --- /dev/null +++ b/tests/unit/app/bootnodes_test.cpp @@ -0,0 +1,118 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "app/bootnodes.hpp" +#include "serde/enr.hpp" +#include "tests/mock/app/chain_spec_mock.hpp" +#include "modules/networking/networking.hpp" + +using ::testing::Return; +using ::testing::ReturnRef; + +/** + * Test fixture for bootnode dependency injection testing + */ +class BootnodeInjectionTest : public ::testing::Test { + protected: + void SetUp() override { + // Create mock chain spec with bootnodes + chain_spec_mock = std::make_shared(); + + // Create test bootnodes + auto addr = libp2p::multi::Multiaddress::create("/ip4/127.0.0.1/tcp/30333"); + ASSERT_TRUE(addr.has_value()); + + // Create a valid peer ID using a proper multihash + std::vector peer_id_bytes = { + 0x12, 0x20, // multihash: sha256 (0x12), length 32 (0x20) + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 + }; + auto peer_id = libp2p::peer::PeerId::fromBytes(peer_id_bytes); + ASSERT_TRUE(peer_id.has_value()); + + lean::app::BootnodeInfo bootnode_info(std::move(addr.value()), std::move(peer_id.value())); + std::vector bootnode_list; + bootnode_list.emplace_back(std::move(bootnode_info)); + + test_bootnodes = lean::app::Bootnodes(std::move(bootnode_list)); + } + + std::shared_ptr chain_spec_mock; + lean::app::Bootnodes test_bootnodes; +}; + +/** + * Test that Bootnodes class correctly stores and retrieves bootnode information + */ +TEST_F(BootnodeInjectionTest, BootnodesStoreAndRetrieve) { + ASSERT_FALSE(test_bootnodes.empty()); + ASSERT_EQ(test_bootnodes.size(), 1); + + const auto& bootnode_list = test_bootnodes.getBootnodes(); + ASSERT_EQ(bootnode_list.size(), 1); + + const auto& bootnode = bootnode_list[0]; + EXPECT_EQ(bootnode.address.getStringAddress(), "/ip4/127.0.0.1/tcp/30333"); +} + +/** + * Test that ChainSpec mock correctly returns bootnodes + */ +TEST_F(BootnodeInjectionTest, ChainSpecReturnsBootnodes) { + EXPECT_CALL(*chain_spec_mock, getBootnodes()) + .WillOnce(ReturnRef(test_bootnodes)); + + const auto& returned_bootnodes = chain_spec_mock->getBootnodes(); + EXPECT_FALSE(returned_bootnodes.empty()); + EXPECT_EQ(returned_bootnodes.size(), 1); +} + +/** + * Test empty bootnodes case + */ +TEST(BootnodeTest, EmptyBootnodes) { + lean::app::Bootnodes empty_bootnodes; + EXPECT_TRUE(empty_bootnodes.empty()); + EXPECT_EQ(empty_bootnodes.size(), 0); + EXPECT_TRUE(empty_bootnodes.getBootnodes().empty()); +} + +/** + * Test ENR parsing and bootnode creation + */ +TEST(BootnodeTest, ENRParsing) { + // Valid ENR from the ENR test suite + std::string test_enr = "enr:-IW4QHcpC4AgOv7WXk1V8E56DDAy6KJ09VMOxSTUwgOqkLF6YihJc5Eoeo4UX1bm9H39Xl-831fomuqR3TZzB3S2IPoBgmlkgnY0gmlwhH8AAAGEcXVpY4InEIlzZWNwMjU2azGhA21sqsJIr5b2r6f5BPVQJToPPvP1qi_mg4qVshZpFGji"; + + // Test ENR parsing directly + auto enr_result = lean::enr::decode(test_enr); + ASSERT_TRUE(enr_result.has_value()) << "ENR parsing should succeed"; + + const auto& enr = enr_result.value(); + auto peer_id = enr.peerId(); + auto connect_addr = enr.connectAddress(); + + EXPECT_FALSE(peer_id.toBase58().empty()); + EXPECT_FALSE(connect_addr.getStringAddress().empty()); + + // Create bootnode info from parsed ENR + lean::app::BootnodeInfo bootnode_info(connect_addr, peer_id); + std::vector bootnode_list; + bootnode_list.emplace_back(std::move(bootnode_info)); + + lean::app::Bootnodes bootnodes(std::move(bootnode_list)); + EXPECT_FALSE(bootnodes.empty()); + EXPECT_EQ(bootnodes.size(), 1); + + const auto& retrieved_bootnodes = bootnodes.getBootnodes(); + EXPECT_EQ(retrieved_bootnodes.size(), 1); + EXPECT_EQ(retrieved_bootnodes[0].peer_id, peer_id); + EXPECT_EQ(retrieved_bootnodes[0].address.getStringAddress(), connect_addr.getStringAddress()); +} \ No newline at end of file diff --git a/tests/unit/blockchain/fork_choice_test.cpp b/tests/unit/blockchain/fork_choice_test.cpp index e7f269e..34639ee 100644 --- a/tests/unit/blockchain/fork_choice_test.cpp +++ b/tests/unit/blockchain/fork_choice_test.cpp @@ -11,10 +11,16 @@ #include #include #include +#include +#include +#include -#include "qtils/test/outcome.hpp" -#include "tests/testutil/prepare_loggers.hpp" #include "blockchain/is_justifiable_slot.hpp" +#include "blockchain/state_transition_function.hpp" +#include "mock/blockchain/validator_registry_mock.hpp" +#include "modules/networking/ssz_snappy.hpp" +#include "qtils/test/outcome.hpp" +#include "testutil/prepare_loggers.hpp" #include "types/signed_block.hpp" using lean::Block; @@ -33,9 +39,9 @@ lean::BlockHash testHash(std::string_view s) { SignedVote makeVote(const Block &source, const Block &target) { return SignedVote{ + .validator_id = 0, .data = { - .validator_id = 0, .slot = target.slot, .head = Checkpoint::from(target), .target = Checkpoint::from(target), @@ -55,33 +61,34 @@ std::optional getVote(const ForkChoiceStore::Votes &votes) { lean::Config config{ .num_validators = 100, - .genesis_time = 1000, + .genesis_time = 1, }; -auto createTestStore(uint64_t time = 100, - lean::Config config_param = config, - lean::BlockHash head = {}, - lean::BlockHash safe_target = {}, - lean::Checkpoint latest_justified = {}, - lean::Checkpoint latest_finalized = {}, - ForkChoiceStore::Blocks blocks = {}, - std::unordered_map states = {}, - ForkChoiceStore::Votes latest_known_votes = {}, - ForkChoiceStore::Votes latest_new_votes = {}, - lean::ValidatorIndex validator_index = 0) { - return ForkChoiceStore( - time, - testutil::prepareLoggers(), - config_param, - head, - safe_target, - latest_justified, - latest_finalized, - blocks, - states, - latest_known_votes, - latest_new_votes, - validator_index); +auto createTestStore( + uint64_t time = 100, + lean::Config config_param = config, + lean::BlockHash head = {}, + lean::BlockHash safe_target = {}, + lean::Checkpoint latest_justified = {}, + lean::Checkpoint latest_finalized = {}, + ForkChoiceStore::Blocks blocks = {}, + std::unordered_map states = {}, + ForkChoiceStore::Votes latest_known_votes = {}, + ForkChoiceStore::Votes latest_new_votes = {}, + lean::ValidatorIndex validator_index = 0) { + return ForkChoiceStore(time, + testutil::prepareLoggers(), + config_param, + head, + safe_target, + latest_justified, + latest_finalized, + blocks, + states, + latest_known_votes, + latest_new_votes, + validator_index, + std::make_shared()); } auto makeBlockMap(std::vector blocks) { @@ -118,8 +125,13 @@ TEST(TestVoteTargetCalculation, test_get_vote_target_basic) { // Recent finalization auto finalized = Checkpoint::from(genesis); - auto store = createTestStore(100, config, block_1.hash(), block_1.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(100, + config, + block_1.hash(), + block_1.hash(), + finalized, + finalized, + makeBlockMap(blocks)); auto target = store.getVoteTarget(); @@ -138,8 +150,13 @@ TEST(TestVoteTargetCalculation, test_vote_target_with_old_finalized) { // Current head is at slot 9 auto &head = blocks.at(9); - auto store = createTestStore(100, config, head.hash(), head.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(100, + config, + head.hash(), + head.hash(), + finalized, + finalized, + makeBlockMap(blocks)); auto target = store.getVoteTarget(); @@ -157,8 +174,13 @@ TEST(TestVoteTargetCalculation, test_vote_target_walks_back_from_head) { // Finalized at genesis auto finalized = Checkpoint::from(genesis); - auto store = createTestStore(100, config, block_2.hash(), block_1.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(100, + config, + block_2.hash(), + block_1.hash(), + finalized, + finalized, + makeBlockMap(blocks)); auto target = store.getVoteTarget(); @@ -177,8 +199,13 @@ TEST(TestVoteTargetCalculation, test_vote_target_justifiable_slot_constraint) { // Head at slot 20 auto &head = blocks.at(20); - auto store = createTestStore(100, config, head.hash(), head.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(100, + config, + head.hash(), + head.hash(), + finalized, + finalized, + makeBlockMap(blocks)); auto target = store.getVoteTarget(); @@ -198,8 +225,13 @@ TEST(TestVoteTargetCalculation, auto finalized = Checkpoint::from(genesis); - auto store = createTestStore(500, config, head.hash(), head.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(500, + config, + head.hash(), + head.hash(), + finalized, + finalized, + makeBlockMap(blocks)); auto target = store.getVoteTarget(); @@ -216,20 +248,19 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_votes) { ForkChoiceStore::Votes votes; votes[0] = SignedVote{ - .data = { - .validator_id = 0, - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(root), - }, + .validator_id = 0, + .data = + { + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), + }, .signature = {}, }; - auto head = getForkChoiceHead(makeBlockMap(blocks), - Checkpoint::from(root), - votes, - 0); + auto head = + getForkChoiceHead(makeBlockMap(blocks), Checkpoint::from(root), votes, 0); EXPECT_EQ(head, target.hash()); } @@ -240,8 +271,8 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_no_votes) { auto &root = blocks.at(0); ForkChoiceStore::Votes empty_votes; - auto head = - getForkChoiceHead(makeBlockMap(blocks), Checkpoint::from(root), empty_votes, 0); + auto head = getForkChoiceHead( + makeBlockMap(blocks), Checkpoint::from(root), empty_votes, 0); EXPECT_EQ(head, root.hash()); } @@ -254,20 +285,19 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_min_score) { ForkChoiceStore::Votes votes; votes[0] = SignedVote{ - .data = { - .validator_id = 0, - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(root), - }, + .validator_id = 0, + .data = + { + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), + }, .signature = {}, }; - auto head = getForkChoiceHead(makeBlockMap(blocks), - Checkpoint::from(root), - votes, - 2); + auto head = + getForkChoiceHead(makeBlockMap(blocks), Checkpoint::from(root), votes, 2); EXPECT_EQ(head, root.hash()); } @@ -281,21 +311,20 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_multiple_votes) { ForkChoiceStore::Votes votes; for (int i = 0; i < 3; ++i) { votes[i] = SignedVote{ - .data = { - .validator_id = static_cast(i), - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(root), - }, + .validator_id = static_cast(i), + .data = + { + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), + }, .signature = {}, }; } - auto head = getForkChoiceHead(makeBlockMap(blocks), - Checkpoint::from(root), - votes, - 0); + auto head = + getForkChoiceHead(makeBlockMap(blocks), Checkpoint::from(root), votes, 0); EXPECT_EQ(head, target.hash()); } @@ -307,8 +336,13 @@ TEST(TestSafeTargetComputation, test_update_safe_target_basic) { auto finalized = Checkpoint::from(genesis); - auto store = createTestStore(100, config, genesis.hash(), genesis.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(100, + config, + genesis.hash(), + genesis.hash(), + finalized, + finalized, + makeBlockMap(blocks)); // Update safe target (this tests the method exists and runs) store.updateSafeTarget(); @@ -327,29 +361,38 @@ TEST(TestSafeTargetComputation, test_safe_target_with_votes) { ForkChoiceStore::Votes new_votes; new_votes[0] = SignedVote{ - .data = { - .validator_id = 0, - .slot = block_1.slot, - .head = Checkpoint::from(block_1), - .target = Checkpoint::from(block_1), - .source = Checkpoint::from(genesis), - }, + .validator_id = 0, + .data = + { + .slot = block_1.slot, + .head = Checkpoint::from(block_1), + .target = Checkpoint::from(block_1), + .source = Checkpoint::from(genesis), + }, .signature = {}, }; new_votes[1] = SignedVote{ - .data = { - .validator_id = 1, - .slot = block_1.slot, - .head = Checkpoint::from(block_1), - .target = Checkpoint::from(block_1), - .source = Checkpoint::from(genesis), - }, + .validator_id = 1, + .data = + { + .slot = block_1.slot, + .head = Checkpoint::from(block_1), + .target = Checkpoint::from(block_1), + .source = Checkpoint::from(genesis), + }, .signature = {}, }; - auto store = createTestStore(100, config, block_1.hash(), genesis.hash(), - finalized, finalized, makeBlockMap(blocks), {}, - {}, new_votes); + auto store = createTestStore(100, + config, + block_1.hash(), + genesis.hash(), + finalized, + finalized, + makeBlockMap(blocks), + {}, + {}, + new_votes); // Update safe target with votes store.updateSafeTarget(); @@ -365,8 +408,13 @@ TEST(TestEdgeCases, test_vote_target_single_block) { auto finalized = Checkpoint::from(genesis); - auto store = createTestStore(100, config, genesis.hash(), genesis.hash(), - finalized, finalized, makeBlockMap(blocks)); + auto store = createTestStore(100, + config, + genesis.hash(), + genesis.hash(), + finalized, + finalized, + makeBlockMap(blocks)); auto target = store.getVoteTarget(); @@ -380,7 +428,8 @@ TEST(TestAttestationValidation, test_validate_attestation_valid) { auto &source = blocks.at(1); auto &target = blocks.at(2); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // Create valid signed vote // Should validate without error @@ -396,7 +445,8 @@ TEST(TestAttestationValidation, test_validate_attestation_slot_order_invalid) { // Earlier than source auto &target = blocks.at(1); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // Create invalid signed vote (source > target slot) EXPECT_OUTCOME_ERROR( @@ -418,7 +468,8 @@ TEST(TestAttestationValidation, auto &source = blocks.at(1); auto &target = blocks.at(2); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // Create signed vote with mismatched checkpoint slot auto vote = makeVote(source, target); @@ -432,9 +483,11 @@ TEST(TestAttestationValidation, test_validate_attestation_too_far_future) { auto &source = blocks.at(1); auto &target = blocks.at(9); - // Use very low genesis time (0) so that target at slot 9 is far in future (slot 9 > current slot + 1) + // Use very low genesis time (0) so that target at slot 9 is far in future + // (slot 9 > current slot + 1) lean::Config low_time_config{.num_validators = 100, .genesis_time = 0}; - auto sample_store = createTestStore(0, low_time_config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(0, low_time_config, {}, {}, {}, {}, makeBlockMap(blocks)); // Create signed vote for future slot (target slot 9 when current is ~0) EXPECT_OUTCOME_ERROR( @@ -447,7 +500,8 @@ TEST(TestAttestationProcessing, test_process_network_attestation) { auto &source = blocks.at(1); auto &target = blocks.at(2); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // Create valid signed vote // Process as network attestation @@ -455,7 +509,8 @@ TEST(TestAttestationProcessing, test_process_network_attestation) { sample_store.processAttestation(makeVote(source, target), false)); // Vote should be added to new votes - EXPECT_EQ(getVote(sample_store.getLatestNewVotes()), Checkpoint::from(target)); + EXPECT_EQ(getVote(sample_store.getLatestNewVotes()), + Checkpoint::from(target)); } // Test processing attestation from a block. @@ -464,7 +519,8 @@ TEST(TestAttestationProcessing, test_process_block_attestation) { auto &source = blocks.at(1); auto &target = blocks.at(2); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // Create valid signed vote // Process as block attestation @@ -482,7 +538,8 @@ TEST(TestAttestationProcessing, test_process_attestation_superseding) { auto &target_1 = blocks.at(1); auto &target_2 = blocks.at(2); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // Process first (older) attestation EXPECT_OUTCOME_SUCCESS( @@ -504,7 +561,8 @@ TEST(TestAttestationProcessing, auto &source = blocks.at(1); auto &target = blocks.at(2); - auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + auto sample_store = + createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); // First process as network vote auto signed_vote = makeVote(source, target); @@ -524,11 +582,12 @@ TEST(TestAttestationProcessing, // Test basic time advancement. TEST(TestTimeAdvancement, test_advance_time_basic) { - // Create a simple store with minimal setup - use 0 time interval so advanceTime is a no-op + // Create a simple store with minimal setup - use 0 time interval so + // advanceTime is a no-op auto sample_store = createTestStore(0, config); // Target time equal to genesis time - should be a no-op - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; // This should not throw an exception and should return empty result auto result = sample_store.advanceTime(target_time); @@ -541,7 +600,7 @@ TEST(TestTimeAdvancement, test_advance_time_no_proposal) { auto sample_store = createTestStore(0, config); // Target time equal to genesis time - should be a no-op - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; // This should not throw an exception and should return empty result auto result = sample_store.advanceTime(target_time); @@ -553,8 +612,8 @@ TEST(TestTimeAdvancement, test_advance_time_already_current) { // Create a simple store with time already set auto sample_store = createTestStore(100, config); - // Target time is in the past relative to current time - should be a no-op - auto current_target = sample_store.getConfig().genesis_time / 1000; + // Target time is in the past relative to current time - should be a no-op + auto current_target = sample_store.getConfig().genesis_time; // Try to advance to past time (should be no-op) auto result = sample_store.advanceTime(current_target); @@ -567,7 +626,7 @@ TEST(TestTimeAdvancement, test_advance_time_small_increment) { auto sample_store = createTestStore(0, config); // Target time equal to genesis time - should be a no-op - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; auto result = sample_store.advanceTime(target_time); EXPECT_TRUE(result.empty()); @@ -580,7 +639,7 @@ TEST(TestTimeAdvancement, test_advance_time_step_by_step) { // Multiple calls to advance time with same target - should all be no-ops for (int i = 1; i <= 5; ++i) { - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; auto result = sample_store.advanceTime(target_time); EXPECT_TRUE(result.empty()); } @@ -593,7 +652,7 @@ TEST(TestTimeAdvancement, test_advance_time_multiple_steps) { // Multiple calls to advance time - should all be no-ops for (int i = 1; i <= 5; ++i) { - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; auto result = sample_store.advanceTime(target_time); EXPECT_TRUE(result.empty()); } @@ -605,7 +664,7 @@ TEST(TestTimeAdvancement, test_advance_time_with_votes) { auto sample_store = createTestStore(0, config); // Advance time - should be no-op - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; auto result = sample_store.advanceTime(target_time); EXPECT_TRUE(result.empty()); } @@ -614,8 +673,13 @@ TEST(TestTimeAdvancement, test_advance_time_with_votes) { TEST(TestHeadSelection, test_get_head_basic) { auto blocks = makeBlocks(1); auto &genesis = blocks.at(0); - auto sample_store = createTestStore(100, config, genesis.hash(), genesis.hash(), - {}, {}, makeBlockMap(blocks)); + auto sample_store = createTestStore(100, + config, + genesis.hash(), + genesis.hash(), + {}, + {}, + makeBlockMap(blocks)); // Get current head auto head = sample_store.getHead(); @@ -630,7 +694,7 @@ TEST(TestHeadSelection, test_advance_time_functionality) { auto sample_store = createTestStore(0, config); // Advance time - should be no-op - auto target_time = sample_store.getConfig().genesis_time / 1000; + auto target_time = sample_store.getConfig().genesis_time; auto result = sample_store.advanceTime(target_time); EXPECT_TRUE(result.empty()); } @@ -640,6 +704,46 @@ TEST(TestHeadSelection, test_produce_block_basic) { // Create a simple store auto sample_store = createTestStore(0, config); - // Try to produce a block - should throw due to missing state, which is expected + // Try to produce a block - should throw due to missing state, which is + // expected EXPECT_THROW(sample_store.produceBlock(1, 1), std::exception); } + +// Test SSZ hash calculation matches ream implementation +TEST(TestSszHashCompatibility, test_genesis_state_hash_matches_ream) { + // Test that our SSZ hash calculation produces the same result as ream's Rust + // implementation. Using the test vector from ream with specific genesis time + // and configuration + + lean::Config test_config{.num_validators = 4, .genesis_time = 1759672259}; + + // Generate genesis state using our standard method + auto genesis_state = lean::STF::generateGenesisState(test_config); + + // Calculate SSZ hash + auto calculated_hash = lean::sszHash(genesis_state); + + // Expected hash from ream's test vector: + // 0xd3e483e76de397f74e4d072fcca01b8d6988b70df60db896537fe4715322cbfd + qtils::ByteArr<32> expected_hash = + qtils::ByteArr<32>::fromHex( + "d3e483e76de397f74e4d072fcca01b8d6988b70df60db896537fe4715322cbfd") + .value(); + + // Verify our SSZ hash calculation matches ream's implementation + EXPECT_EQ(calculated_hash, expected_hash) + << "Genesis state SSZ hash does not match ream implementation. " + << "This indicates incompatibility in SSZ serialization between C++ and " + "Rust implementations."; + + Block block = lean::STF::genesisBlock(genesis_state); + qtils::ByteArr<32> expected_block_root = + qtils::ByteArr<32>::fromHex( + "aacb28e55e6c6a17fd7c61971d98844350474727af7e9bdadf337750bd11d41f") + .value(); + auto calculated_block_root = lean::sszHash(block); + EXPECT_EQ(calculated_block_root, expected_block_root) + << "Genesis block SSZ hash does not match ream implementation. " + << "This indicates incompatibility in SSZ serialization between C++ and " + "Rust implementations."; +} diff --git a/tests/unit/blockchain/state_transition_function_test.cpp b/tests/unit/blockchain/state_transition_function_test.cpp index aa9bf0c..3d644c2 100644 --- a/tests/unit/blockchain/state_transition_function_test.cpp +++ b/tests/unit/blockchain/state_transition_function_test.cpp @@ -28,10 +28,9 @@ TEST(STF, Test) { .proposer_index = 1, .parent_root = block0.hash(), }; - auto state1 = stf.stateTransition({.message = block1}, state0, false).value(); + auto state1 = stf.stateTransition(block1, state0, false).value(); block1.state_root = lean::sszHash(state1); - auto state1_apply = - stf.stateTransition({.message = block1}, state0, true).value(); + auto state1_apply = stf.stateTransition(block1, state0, true).value(); EXPECT_EQ(state1_apply, state1); block1.setHash(); @@ -39,10 +38,9 @@ TEST(STF, Test) { .slot = block1.slot + 3, .parent_root = block1.hash(), }; - auto state2 = stf.stateTransition({.message = block2}, state1, false).value(); + auto state2 = stf.stateTransition(block2, state1, false).value(); block2.state_root = lean::sszHash(state2); - auto state2_apply = - stf.stateTransition({.message = block2}, state1, true).value(); + auto state2_apply = stf.stateTransition(block2, state1, true).value(); EXPECT_EQ(state2_apply, state2); block2.setHash(); } diff --git a/tests/unit/serde/enr_test.cpp b/tests/unit/serde/enr_test.cpp index a241c0d..7a4d969 100644 --- a/tests/unit/serde/enr_test.cpp +++ b/tests/unit/serde/enr_test.cpp @@ -56,8 +56,8 @@ TEST(EnrTest, EncodeDecodeRoundTrip) { // IP and port present and correct ASSERT_TRUE(enr.ip.has_value()); - // encode() uses Ip{1,0,0,127} - Ip expected_ip{1, 0, 0, 127}; + // encode() uses Ip{127, 0, 0, 1} + Ip expected_ip{127, 0, 0, 1}; EXPECT_EQ(enr.ip.value(), expected_ip); ASSERT_TRUE(enr.port.has_value()); @@ -94,7 +94,7 @@ TEST(EnrTest, DecodeGivenEnrAddress) { // Provided ENR string from user request // clang-format off std::string_view addr = - "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg"; + "enr:-IW4QHcpC4AgOv7WXk1V8E56DDAy6KJ09VMOxSTUwgOqkLF6YihJc5Eoeo4UX1bm9H39Xl-831fomuqR3TZzB3S2IPoBgmlkgnY0gmlwhH8AAAGEcXVpY4InEIlzZWNwMjU2azGhA21sqsJIr5b2r6f5BPVQJToPPvP1qi_mg4qVshZpFGji"; // clang-format on ASSERT_OUTCOME_SUCCESS(enr, lean::enr::decode(addr)); @@ -104,12 +104,12 @@ TEST(EnrTest, DecodeGivenEnrAddress) { // IP should be present and equal to 3.17.30.69 ASSERT_TRUE(enr.ip.has_value()); - Ip expected_ip{3, 17, 30, 69}; + Ip expected_ip{127, 0, 0, 1}; EXPECT_EQ(enr.ip.value(), expected_ip); - // UDP port should be present; expected commonly used 9000 + // UDP port should be present; expected 10000 ASSERT_TRUE(enr.port.has_value()); - EXPECT_EQ(enr.port.value(), static_cast(9000)); + EXPECT_EQ(enr.port.value(), static_cast(10000)); // Public key must be compressed secp256k1 (33 bytes) with a valid prefix EXPECT_EQ(enr.public_key.size(), 33u); diff --git a/tests/unit/utils/CMakeLists.txt b/tests/unit/utils/CMakeLists.txt new file mode 100644 index 0000000..61cd511 --- /dev/null +++ b/tests/unit/utils/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +addtest(validator_registry_test + validator_registry_test.cpp +) +target_link_libraries(validator_registry_test + validator_registry + logger_for_tests +) diff --git a/tests/unit/utils/validator_registry_test.cpp b/tests/unit/utils/validator_registry_test.cpp new file mode 100644 index 0000000..c87e583 --- /dev/null +++ b/tests/unit/utils/validator_registry_test.cpp @@ -0,0 +1,62 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "blockchain/impl/validator_registry_impl.hpp" +#include "testutil/prepare_loggers.hpp" + +TEST(ValidatorRegistryTest, LoadsRegistryAndResolvesIndices) { + auto logger = testutil::prepareLoggers(); + + lean::ValidatorRegistryImpl registry{ + logger, + R"( +node_0: + - 0 +node_1: + - 1 +)", + "node_1", + }; + + EXPECT_EQ(registry.currentValidatorIndices(), + lean::ValidatorRegistry::ValidatorIndices{1}); + EXPECT_EQ(registry.nodeIdByIndex(0), std::optional{"node_0"}); + EXPECT_EQ(registry.nodeIdByIndex(1), std::optional{"node_1"}); +} + +TEST(ValidatorRegistryTest, MissingNodeDefaultsToZero) { + auto logger = testutil::prepareLoggers(); + + lean::ValidatorRegistryImpl registry{ + logger, + R"( +node_0: + - 0 +)", + "unknown_node", + }; + + EXPECT_EQ(registry.currentValidatorIndices(), + lean::ValidatorRegistry::ValidatorIndices{}); +} + +TEST(ValidatorRegistryTest, ThrowsOnDuplicateIndex) { + auto logger = testutil::prepareLoggers(); + + EXPECT_THROW((lean::ValidatorRegistryImpl{ + logger, + R"( +node_0: + - 0 +node_1: + - 0 +)", + "node_0", + }), + std::runtime_error); +} diff --git a/vcpkg-overlay/leanp2p/portfile.cmake b/vcpkg-overlay/leanp2p/portfile.cmake index b875835..ad30b84 100644 --- a/vcpkg-overlay/leanp2p/portfile.cmake +++ b/vcpkg-overlay/leanp2p/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/leanp2p - REF 9dcfed625f1e6c51e8d0f986064884477dc44390 - SHA512 8d0470ca47753ad7c5f5af2937f662229e7a74829e24533e88213dabb8a3f3ae8524f36f690a72899af8fcfbebc98729348c09a10e7535ef1473d8697d25a652 + REF refs/tags/v0.0.2 + SHA512 72a82d313ca84fe0095c755daa733cfdd09609080a6a9f627587355d57d0bf4a34448884282f29ebd1e23fcc21d8f5181ada63ff3d87772e060e5e9f554406a2 ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() diff --git a/vcpkg-overlay/sszpp/vcpkg.patch b/vcpkg-overlay/sszpp/vcpkg.patch index 738b9e6..ae18634 100644 --- a/vcpkg-overlay/sszpp/vcpkg.patch +++ b/vcpkg-overlay/sszpp/vcpkg.patch @@ -120,19 +120,52 @@ index 3174735..b086711 100644 } diff --git a/lib/container.hpp b/lib/container.hpp -index dc31773..3e42de7 100644 +index dc31773..c1e1e57 100644 --- a/lib/container.hpp +++ b/lib/container.hpp -@@ -46,7 +46,7 @@ struct is_ssz_object : std::true_type {}; +@@ -31,6 +31,8 @@ + namespace ssz { + + struct ssz_container { ++ struct variable_size : std::false_type {}; ++ + constexpr auto operator<=>(const ssz_container &rhs) const noexcept = default; + constexpr bool operator==(const ssz_container &rhs) const noexcept = default; + }; +@@ -43,15 +45,24 @@ template + requires std::derived_from + struct is_ssz_object : std::true_type {}; + ++template ++struct tuple_all_fixed_size; ++template ++struct tuple_all_fixed_size> : std::bool_constant<(ssz_object_fixed_size and ...)> {}; ++ // Serialization - #define SSZ_CONT(...) \ - constexpr std::size_t ssz_size() const noexcept { return ssz::compute_total_length(__VA_ARGS__); } \ +-#define SSZ_CONT(...) \ +- constexpr std::size_t ssz_size() const noexcept { return ssz::compute_total_length(__VA_ARGS__); } \ - constexpr void serialize(ssz::ssz_iterator auto result) const { ssz::serialize(result, __VA_ARGS__); } \ +- constexpr void deserialize(const std::ranges::sized_range auto &bytes) { \ +- ssz::deserialize_container(bytes, __VA_ARGS__); \ +- } \ +- void hash_tree_root(ssz::ssz_iterator auto result, size_t cpu_count = 0) const { \ +- ssz::_container_hash(result, cpu_count, __VA_ARGS__); \ ++#define SSZ_CONT(...) \ ++ constexpr std::size_t ssz_size() const noexcept { return ssz::compute_total_length(__VA_ARGS__); } \ + constexpr void serialize(ssz::ssz_iterator auto result) const { ssz::serialize_container(result, __VA_ARGS__); } \ - constexpr void deserialize(const std::ranges::sized_range auto &bytes) { \ - ssz::deserialize_container(bytes, __VA_ARGS__); \ - } \ -@@ -75,7 +75,7 @@ constexpr std::uint32_t compute_total_length(const ssz_object auto &...members) ++ constexpr void deserialize(const std::ranges::sized_range auto &bytes) { \ ++ ssz::deserialize_container(bytes, __VA_ARGS__); \ ++ } \ ++ void hash_tree_root(ssz::ssz_iterator auto result, size_t cpu_count = 0) const { \ ++ ssz::_container_hash(result, cpu_count, __VA_ARGS__); \ ++ } \ ++ void assert_consistent_variable_size() const { \ ++ auto t = std::tie(__VA_ARGS__); \ ++ static_assert(variable_size::value or ssz::tuple_all_fixed_size::value); \ + } + #ifdef HAVE_YAML + #define YAML_CONT(...) \ +@@ -75,7 +86,7 @@ constexpr std::uint32_t compute_total_length(const ssz_object auto &...members) return (... + size_plus_placeholder(members)); } @@ -141,6 +174,15 @@ index dc31773..3e42de7 100644 auto fsize = compute_fixed_length(members...); auto variable = result + fsize; auto begin = result; +@@ -174,7 +185,7 @@ namespace _detail { + auto _decode_member = [](const YAML::Node &node, yaml_pair auto pair) { + return YAML::convert>::decode(node[pair.first], pair.second); + }; +-} ++} // namespace _detail + + bool yaml_decode_container(const YAML::Node &node, yaml_pair auto... pairs) { + return (_detail::_decode_member(node, pairs), ...); diff --git a/lib/cxx23/ranges/chunk.hpp b/lib/cxx23/ranges/chunk.hpp new file mode 100644 index 0000000..4c73bd2